Compare commits
5 Commits
44c6437b64
...
feat-1.1.0
| Author | SHA1 | Date | |
|---|---|---|---|
| a15b3f8027 | |||
| 417116b928 | |||
| a6387f8220 | |||
| 2a38778fa3 | |||
| bab02e1efd |
@@ -95,6 +95,6 @@ pnpm test # 全量,只在改了 t
|
||||
|
||||
## 脚注
|
||||
|
||||
`footnote()` 单行约 60 字符封顶(1200px 画布),超了用 `\n` 手动断行。结尾统一 `| @思考的Joey`。
|
||||
`footnote()` 单行约 60 字符封顶(1200px 画布),超了用 `\n` 手动断行。结尾统一 `| @Joey知行记`。
|
||||
|
||||
水印文案在 `theme.js` 的 `WATERMARK_TEXT`,改一处全局生效。
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# 财报图表 · Joey 谨制
|
||||
|
||||
基于 **ECharts 5.5** 的财报数据可视化项目,模仿"方木博制"风格。
|
||||
基于 **ECharts 5.5** 的财报数据可视化项目,采用“Joey 风格”。
|
||||
|
||||
修改 CSV 数据,刷新页面即可更新图表;每张图表都可独立导出为高清 PNG。
|
||||
|
||||
|
||||
+187
-2
@@ -28,7 +28,10 @@ button {
|
||||
.sidebar-title { margin: 0; }
|
||||
|
||||
.reload-button,
|
||||
.export-button {
|
||||
.export-button,
|
||||
.cart-button,
|
||||
.cart-export-button,
|
||||
.primary-button {
|
||||
padding: 7px 13px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 6px;
|
||||
@@ -57,6 +60,186 @@ button {
|
||||
.reload-button:hover { opacity: .88; transform: rotate(-45deg); }
|
||||
.export-button:hover { color: var(--accent); border-color: var(--accent); }
|
||||
|
||||
/* ── 合并队列(购物车)────────────────────────────── */
|
||||
|
||||
.cart-button { font-size: 12px; }
|
||||
.cart-button:hover { color: var(--accent); border-color: var(--accent); }
|
||||
.cart-button[data-in-cart="true"] {
|
||||
color: #fff;
|
||||
background: var(--accent);
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.cart-bar[hidden] { display: none; }
|
||||
|
||||
.cart-bar {
|
||||
position: fixed;
|
||||
left: 50%;
|
||||
bottom: 18px;
|
||||
transform: translateX(-50%);
|
||||
z-index: 20;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
max-width: min(1160px, calc(100vw - 32px));
|
||||
padding: 9px 12px;
|
||||
background: rgba(255, 255, 255, .97);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 6px 24px rgba(0, 0, 0, .12);
|
||||
}
|
||||
|
||||
.cart-label {
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.cart-chips {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
max-width: 44vw;
|
||||
overflow-x: auto;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
.cart-chips::-webkit-scrollbar { display: none; }
|
||||
|
||||
.cart-chip {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 0 0 auto;
|
||||
gap: 2px;
|
||||
padding: 3px 4px 3px 10px;
|
||||
background: var(--accent-soft);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.cart-chip-name {
|
||||
max-width: 150px;
|
||||
overflow: hidden;
|
||||
color: var(--ink);
|
||||
font-size: 12px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.cart-chip-button {
|
||||
padding: 2px 4px;
|
||||
border: 0;
|
||||
border-radius: 4px;
|
||||
background: transparent;
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.cart-chip-button:hover:not(:disabled) { color: var(--accent); }
|
||||
.cart-chip-button:disabled { opacity: .3; cursor: default; }
|
||||
|
||||
.cart-divider { width: 1px; height: 22px; background: var(--line); }
|
||||
|
||||
.cart-modes {
|
||||
display: flex;
|
||||
flex: 0 0 auto;
|
||||
padding: 2px;
|
||||
background: #f4f2ee;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.cart-mode-button {
|
||||
padding: 4px 8px;
|
||||
border: 0;
|
||||
border-radius: 5px;
|
||||
background: transparent;
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.cart-mode-button[data-active="true"] {
|
||||
color: var(--accent);
|
||||
background: var(--surface);
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, .08);
|
||||
}
|
||||
|
||||
.cart-export-button { color: #fff; background: var(--accent); border-color: var(--accent); white-space: nowrap; }
|
||||
.cart-export-button:hover { opacity: .9; }
|
||||
.cart-clear-button { padding: 7px 8px; border: 0; background: transparent; color: var(--muted); font-size: 12px; white-space: nowrap; }
|
||||
.cart-clear-button:hover { color: #a63f32; }
|
||||
|
||||
/* ── 合并预览弹窗 ────────────────────────────────── */
|
||||
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 30;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
padding: 24px;
|
||||
background: rgba(51, 51, 51, .45);
|
||||
}
|
||||
|
||||
.modal {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: min(860px, 100%);
|
||||
max-height: calc(100vh - 48px);
|
||||
background: var(--surface);
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.modal-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid var(--line);
|
||||
}
|
||||
|
||||
.modal-title { margin: 0; font-size: 14px; }
|
||||
.modal-notice { color: #a67c2b; font-size: 12px; }
|
||||
|
||||
.modal-close {
|
||||
margin-left: auto;
|
||||
padding: 2px 8px;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: var(--muted);
|
||||
font-size: 16px;
|
||||
}
|
||||
.modal-close:hover { color: var(--ink); }
|
||||
|
||||
.modal-body {
|
||||
display: grid;
|
||||
gap: 20px;
|
||||
overflow-y: auto;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.preview-item {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
justify-items: center;
|
||||
}
|
||||
|
||||
.preview-canvas {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.modal-foot {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
padding: 12px 16px;
|
||||
border-top: 1px solid var(--line);
|
||||
}
|
||||
|
||||
.primary-button { color: #fff; background: var(--accent); border-color: var(--accent); }
|
||||
.primary-button:hover { opacity: .9; }
|
||||
|
||||
.app-layout {
|
||||
display: grid;
|
||||
grid-template-columns: 184px minmax(0, 1fr) 184px;
|
||||
@@ -235,7 +418,9 @@ button {
|
||||
.tab-button { flex: 0 0 auto; }
|
||||
.card { scroll-margin-top: 120px; }
|
||||
.card-bar { align-items: flex-start; flex-wrap: wrap; }
|
||||
.export-button { margin-left: auto; }
|
||||
.cart-button { margin-left: auto; }
|
||||
.cart-bar { left: 12px; right: 12px; transform: none; flex-wrap: wrap; }
|
||||
.cart-chips { max-width: none; }
|
||||
.chart { min-width: 560px; }
|
||||
.card { overflow-x: auto; }
|
||||
}
|
||||
|
||||
@@ -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,11 @@
|
||||
period,total_assets,equity,liabilities,cash_like,cash,receivables,inventory
|
||||
2016,,,,,,,
|
||||
2017,,,,,,,
|
||||
2018,,,,,,,
|
||||
2019,,,,,,,
|
||||
2020,,,,,,,
|
||||
2021,,,,,,,
|
||||
2022,,,,,,,
|
||||
2023,,,,,,,
|
||||
2024,,,,,,,
|
||||
2025,,,,,,,
|
||||
|
@@ -0,0 +1,22 @@
|
||||
period,total_assets,equity,liabilities,cash_like,cash,receivables,inventory
|
||||
2021Q2,,,,,,,
|
||||
2021Q3,,,,,,,
|
||||
2021Q4,,,,,,,
|
||||
2022Q1,,,,,,,
|
||||
2022Q2,,,,,,,
|
||||
2022Q3,,,,,,,
|
||||
2022Q4,,,,,,,
|
||||
2023Q1,,,,,,,
|
||||
2023Q2,,,,,,,
|
||||
2023Q3,,,,,,,
|
||||
2023Q4,,,,,,,
|
||||
2024Q1,,,,,,,
|
||||
2024Q2,,,,,,,
|
||||
2024Q3,,,,,,,
|
||||
2024Q4,,,,,,,
|
||||
2025Q1,,,,,,,
|
||||
2025Q2,,,,,,,
|
||||
2025Q3,,,,,,,
|
||||
2025Q4,,,,,,,
|
||||
2026Q1,,,,,,,
|
||||
2026Q2,,,,,,,
|
||||
|
@@ -0,0 +1,31 @@
|
||||
period,planned_capacity,released_capacity,base_wine_output,commercial_wine_sales
|
||||
2000,,,5397.000000,
|
||||
2001,,,7317.000000,
|
||||
2002,,,8640.000000,
|
||||
2003,,,9757.000000,
|
||||
2004,,,11522.000000,6500.000000
|
||||
2005,,,12500.000000,7356.000000
|
||||
2006,,,13839.000000,7300.000000
|
||||
2007,,,16865.000000,8640.000000
|
||||
2008,,,20431.000000,9558.000000
|
||||
2009,,,23004.000000,10380.000000
|
||||
2010,,,26284.000000,10141.000000
|
||||
2011,,,30026.000000,11250.000000
|
||||
2012,,,33600.000000,14800.000000
|
||||
2013,,,38452.000000,15500.000000
|
||||
2014,,,38745.000000,18000.000000
|
||||
2015,,,32179.000000,19798.000000
|
||||
2016,32592.000000,31360.000000,39313.000000,22918.000000
|
||||
2017,35952.000000,32592.000000,42829.000000,30206.000000
|
||||
2018,37408.000000,35952.000000,49672.000000,32464.000000
|
||||
2019,38528.000000,37408.000000,49923.000000,34562.000000
|
||||
2020,42560.000000,38528.000000,50235.000000,34313.000000
|
||||
2021,42743.000000,42743.000000,56472.250000,36261.000000
|
||||
2022,42743.000000,42743.000000,56810.440000,37901.000000
|
||||
2023,42795.000000,42743.000000,57204.110000,42111.000000
|
||||
2024,44595.000000,,56271.990000,46412.950000
|
||||
2025,46395.000000,,58473.160000,46750.660000
|
||||
2026,,,,
|
||||
2027,,,,
|
||||
2028,,,,
|
||||
2029,,,,
|
||||
|
@@ -0,0 +1,11 @@
|
||||
period,operating_cash_flow,investing_cash_flow,financing_cash_flow,net_cash_increase
|
||||
2016,374.510000,-11.030000,-83.350000,280.140000
|
||||
2017,221.530000,-11.210000,-88.990000,121.330000
|
||||
2018,413.850000,-16.290000,-164.410000,233.150000
|
||||
2019,452.110000,-31.660000,-192.840000,227.610000
|
||||
2020,516.690000,-18.050000,-241.280000,257.370000
|
||||
2021,640.290000,-55.620000,-265.640000,319.000000
|
||||
2022,366.990000,-55.370000,-574.250000,-262.620000
|
||||
2023,665.930000,-97.240000,-588.890000,-20.190000
|
||||
2024,924.640000,-17.850000,-710.680000,196.100000
|
||||
2025,615.220000,-316.420000,-734.270000,-435.440000
|
||||
|
@@ -0,0 +1,22 @@
|
||||
period,operating_cash_flow,investing_cash_flow,financing_cash_flow,net_cash_increase
|
||||
2021Q2,217.190000,-38.720000,-264.080000,-85.610000
|
||||
2021Q3,150.330000,-3.940000,-1.200000,145.180000
|
||||
2021Q4,272.770000,-12.960000,-0.360000,259.430000
|
||||
2022Q1,-68.760000,-12.400000,-0.040000,-81.200000
|
||||
2022Q2,68.650000,-4.650000,-283.150000,-219.160000
|
||||
2022Q3,94.160000,-9.660000,-15.460000,69.050000
|
||||
2022Q4,272.940000,-28.660000,-275.600000,-31.310000
|
||||
2023Q1,52.450000,2.180000,-0.230000,54.400000
|
||||
2023Q2,251.420000,-6.980000,-325.740000,-81.290000
|
||||
2023Q3,196.150000,-78.360000,-21.280000,96.500000
|
||||
2023Q4,165.910000,-14.080000,-241.640000,-89.800000
|
||||
2024Q1,91.870000,-7.670000,-0.120000,84.080000
|
||||
2024Q2,274.350000,-21.320000,-388.040000,-135.010000
|
||||
2024Q3,77.990000,22.940000,-1.730000,99.200000
|
||||
2024Q4,480.430000,-11.800000,-320.790000,147.830000
|
||||
2025Q1,88.090000,-6.240000,-16.070000,65.780000
|
||||
2025Q2,43.100000,3.100000,-382.860000,-336.630000
|
||||
2025Q3,250.780000,-51.090000,-33.510000,166.170000
|
||||
2025Q4,233.250000,-262.190000,-301.830000,-330.760000
|
||||
2026Q1,269.100000,256.390000,-10.060000,515.430000
|
||||
2026Q2,437.810000,0.020000,-369.380000,68.420000
|
||||
|
@@ -0,0 +1,11 @@
|
||||
period,direct,wholesale,direct_share,wholesale_share
|
||||
2016,34.850000,353.560000,0.089725,0.910275
|
||||
2017,,,,
|
||||
2018,43.760000,691.890000,0.059485,0.940515
|
||||
2019,72.490000,780.960000,0.084938,0.915062
|
||||
2020,132.400000,815.820000,0.139630,0.860370
|
||||
2021,240.290000,820.300000,0.226563,0.773437
|
||||
2022,493.790000,743.940000,0.398948,0.601052
|
||||
2023,672.330000,799.860000,0.456687,0.543313
|
||||
2024,748.433270,957.685110,0.438676,0.561324
|
||||
2025,845.430319,842.315533,0.500923,0.499077
|
||||
|
@@ -0,0 +1,31 @@
|
||||
period,moutai,series,direct,wholesale,moutai_share,direct_share
|
||||
2019Q1,194.980000,21.320000,10.920000,205.380000,0.901433,0.050485
|
||||
2019Q2,347.950000,46.550000,16.020000,378.480000,0.882003,0.040608
|
||||
2019Q3,538.320000,70.380000,31.030000,577.670000,0.884377,0.050977
|
||||
2019Q4,758.020000,95.420000,72.490000,780.960000,0.888194,0.084938
|
||||
2020Q1,222.220000,21.690000,19.380000,224.530000,0.911074,0.079456
|
||||
2020Q2,392.610000,46.500000,51.530000,387.590000,0.894104,0.117348
|
||||
2020Q3,601.450000,70.000000,84.330000,587.130000,0.895748,0.125592
|
||||
2020Q4,848.310000,99.910000,132.400000,815.820000,0.894634,0.139630
|
||||
2021Q1,245.920000,26.430000,47.770000,224.570000,0.902956,0.175406
|
||||
2021Q2,429.490000,60.600000,95.040000,395.050000,0.876349,0.193924
|
||||
2021Q3,649.920000,95.400000,146.850000,598.470000,0.872001,0.197029
|
||||
2021Q4,934.650000,125.950000,240.290000,820.300000,0.881246,0.226563
|
||||
2022Q1,288.600000,34.280000,108.870000,214.010000,0.893831,0.337184
|
||||
2022Q2,499.650000,75.980000,209.490000,366.140000,0.868005,0.363932
|
||||
2022Q3,744.000000,125.400000,318.820000,550.590000,0.855763,0.366708
|
||||
2022Q4,1078.340000,159.390000,493.790000,743.940000,0.871224,0.398948
|
||||
2023Q1,337.220000,50.140000,178.070000,209.290000,0.870560,0.459702
|
||||
2023Q2,592.790000,100.740000,314.200000,379.330000,0.854743,0.453045
|
||||
2023Q3,872.700000,155.940000,462.070000,566.570000,0.848402,0.449205
|
||||
2023Q4,1265.890000,206.300000,672.330000,799.860000,0.859869,0.456687
|
||||
2024Q1,397.070000,59.360000,193.190000,263.240000,0.869947,0.423263
|
||||
2024Q2,685.670000,131.470000,337.280000,479.860000,0.839110,0.412757
|
||||
2024Q3,1011.260000,193.930000,519.890000,685.290000,0.839088,0.431380
|
||||
2024Q4,1459.280000,246.840000,748.430000,957.690000,0.855321,0.438674
|
||||
2025Q1,435.570000,70.220000,232.200000,273.600000,0.861168,0.459075
|
||||
2025Q2,755.900000,137.630000,400.100000,493.430000,0.845970,0.447775
|
||||
2025Q3,1105.140000,178.840000,555.550000,728.420000,0.860714,0.432681
|
||||
2025Q4,1465.000000,222.750000,845.430000,842.320000,0.868020,0.500921
|
||||
2026Q1,460.048686,78.808783,295.040339,243.817130,0.853748,0.547529
|
||||
2026Q2,777.240000,129.340000,519.620000,386.970000,0.857332,0.573159
|
||||
|
@@ -0,0 +1,31 @@
|
||||
period,moutai,series,direct,wholesale,moutai_share,direct_share
|
||||
2019Q1,194.980000,21.320000,10.920000,205.380000,0.901433,0.050485
|
||||
2019Q2,152.970000,25.230000,5.100000,173.100000,0.858418,0.028620
|
||||
2019Q3,190.380000,23.830000,15.010000,199.190000,0.888754,0.070075
|
||||
2019Q4,219.700000,25.050000,41.460000,203.290000,0.897651,0.169397
|
||||
2020Q1,222.220000,21.690000,19.380000,224.530000,0.911074,0.079456
|
||||
2020Q2,170.390000,24.820000,32.140000,163.060000,0.872855,0.164652
|
||||
2020Q3,208.840000,23.500000,32.800000,199.540000,0.898855,0.141172
|
||||
2020Q4,246.860000,29.910000,48.070000,228.690000,0.891932,0.173688
|
||||
2021Q1,245.920000,26.430000,47.770000,224.570000,0.902956,0.175406
|
||||
2021Q2,183.570000,34.180000,47.270000,170.480000,0.843031,0.217084
|
||||
2021Q3,220.430000,34.790000,51.810000,203.410000,0.863686,0.203001
|
||||
2021Q4,284.730000,30.550000,93.440000,221.830000,0.903102,0.296381
|
||||
2022Q1,288.600000,34.280000,108.870000,214.010000,0.893831,0.337184
|
||||
2022Q2,211.050000,41.700000,100.620000,152.130000,0.835015,0.398101
|
||||
2022Q3,244.350000,49.420000,109.320000,184.450000,0.831773,0.372128
|
||||
2022Q4,334.340000,33.980000,174.970000,193.350000,0.907743,0.475049
|
||||
2023Q1,337.220000,50.140000,178.070000,209.290000,0.870560,0.459702
|
||||
2023Q2,255.570000,50.600000,136.130000,170.040000,0.834732,0.444622
|
||||
2023Q3,279.910000,55.200000,147.870000,187.240000,0.835278,0.441258
|
||||
2023Q4,393.200000,50.360000,210.260000,233.290000,0.886464,0.474039
|
||||
2024Q1,397.070000,59.360000,193.190000,263.240000,0.869947,0.423263
|
||||
2024Q2,288.600000,72.110000,144.090000,216.620000,0.800089,0.399462
|
||||
2024Q3,325.590000,62.460000,182.610000,205.430000,0.839041,0.470596
|
||||
2024Q4,448.020000,52.910000,228.540000,272.390000,0.894376,0.456231
|
||||
2025Q1,435.570000,70.220000,232.200000,273.600000,0.861168,0.459075
|
||||
2025Q2,320.320000,67.400000,167.890000,219.830000,0.826163,0.433019
|
||||
2025Q3,349.240000,41.220000,155.460000,235.000000,0.894432,0.398146
|
||||
2025Q4,359.860000,43.900000,289.880000,113.890000,0.891272,0.717933
|
||||
2026Q1,460.048686,78.808783,295.040339,243.817130,0.853748,0.547529
|
||||
2026Q2,317.190000,50.530000,224.580000,143.150000,0.862586,0.610720
|
||||
|
@@ -0,0 +1,10 @@
|
||||
period,domestic,overseas,total
|
||||
2022年报,2084.000000,105.000000,2189.000000
|
||||
2023年报,2080.000000,106.000000,2186.000000
|
||||
2024年报,2143.000000,104.000000,2247.000000
|
||||
2025Q1,2174.000000,109.000000,2283.000000
|
||||
2025Q2,2280.000000,115.000000,2395.000000
|
||||
2025Q3,2325.000000,121.000000,2446.000000
|
||||
2025年报,2353.000000,126.000000,2479.000000
|
||||
2026Q1,2098.000000,124.000000,2222.000000
|
||||
2026Q2,2307.000000,119.000000,2426.000000
|
||||
|
@@ -0,0 +1,27 @@
|
||||
period,dividend,net_profit,payout_ratio
|
||||
2001,1.500000,3.282907,0.456912
|
||||
2002,0.550000,3.767985,0.145967
|
||||
2003,0.907500,5.867478,0.154666
|
||||
2004,1.966250,8.205540,0.239625
|
||||
2005,1.415700,11.577382,0.122282
|
||||
2006,12.184458,15.448124,0.788734
|
||||
2007,7.890168,28.308316,0.278723
|
||||
2008,10.910328,37.994806,0.287153
|
||||
2009,11.184030,43.081146,0.259604
|
||||
2010,21.707400,50.511942,0.429748
|
||||
2011,41.496055,87.631459,0.473529
|
||||
2012,66.640774,133.080796,0.500754
|
||||
2013,45.409993,151.366398,0.300000
|
||||
2014,49.950993,153.498043,0.325418
|
||||
2015,77.519966,155.030903,0.500029
|
||||
2016,85.258145,167.183627,0.509967
|
||||
2017,138.169196,270.793603,0.510238
|
||||
2018,182.638598,352.036253,0.518806
|
||||
2019,213.867676,412.064710,0.519015
|
||||
2020,242.358242,466.972854,0.518999
|
||||
2021,272.280873,525.811027,0.517830
|
||||
2022,547.513811,627.164437,0.872999
|
||||
2023,627.872784,747.340716,0.840143
|
||||
2024,646.720000,862.281464,0.750010
|
||||
2025,650.330000,823.200671,0.790002
|
||||
2025,300.006375,646.267467,0.464214
|
||||
|
@@ -0,0 +1,23 @@
|
||||
period,sales_expense,admin_expense,rd_expense,tax_surcharge,revenue,sales_expense_rate,admin_expense_rate,rd_expense_rate,tax_surcharge_rate
|
||||
2004,,,,,,,,,
|
||||
2005,,,,,,,,,
|
||||
2006,,,,,,,,,
|
||||
2007,,,,,,,,,
|
||||
2008,,,,,,,,,
|
||||
2009,,,,,,,,,
|
||||
2010,,,,,,,,,
|
||||
2011,,,,,,,,,
|
||||
2012,,,,,,,,,
|
||||
2013,,,,,,,,,
|
||||
2014,,,,,,,,,
|
||||
2015,,,,,,,,,
|
||||
2016,,,,,,,,,
|
||||
2017,,,,,,,,,
|
||||
2018,,,,,,,,,
|
||||
2019,,,,,,,,,
|
||||
2020,,,,,,,,,
|
||||
2021,,,,,,,,,
|
||||
2022,,,,,,,,,
|
||||
2023,,,,,,,,,
|
||||
2024,,,,,,,,,
|
||||
2025,,,,,,,,,
|
||||
|
@@ -0,0 +1,27 @@
|
||||
period,sales_expense,admin_expense,rd_expense,tax_surcharge,revenue,gross_margin,sales_expense_rate,admin_expense_rate,rd_expense_rate,tax_surcharge_rate
|
||||
2020Q1,,,,,,,,,,
|
||||
2020Q2,,,,,,,,,,
|
||||
2020Q3,,,,,,,,,,
|
||||
2020Q4,,,,,,,,,,
|
||||
2021Q1,,,,,,,,,,
|
||||
2021Q2,,,,,,,,,,
|
||||
2021Q3,,,,,,,,,,
|
||||
2021Q4,,,,,,,,,,
|
||||
2022Q1,,,,,,,,,,
|
||||
2022Q2,,,,,,,,,,
|
||||
2022Q3,,,,,,,,,,
|
||||
2022Q4,,,,,,,,,,
|
||||
2023Q1,,,,,,,,,,
|
||||
2023Q2,,,,,,,,,,
|
||||
2023Q3,,,,,,,,,,
|
||||
2023Q4,,,,,,,,,,
|
||||
2024Q1,,,,,,,,,,
|
||||
2024Q2,,,,,,,,,,
|
||||
2024Q3,,,,,,,,,,
|
||||
2024Q4,,,,,,,,,,
|
||||
2025Q1,,,,,,,,,,
|
||||
2025Q2,,,,,,,,,,
|
||||
2025Q3,,,,,,,,,,
|
||||
2025Q4,,,,,,,,,,
|
||||
2026Q1,,,,,,,,,,
|
||||
2026Q2,,,,,,,,,,
|
||||
|
@@ -0,0 +1,17 @@
|
||||
period,wholesale_price
|
||||
2010,1000.000000
|
||||
2011,1200.000000
|
||||
2012,2300.000000
|
||||
2013,1500.000000
|
||||
2014,1000.000000
|
||||
2015,850.000000
|
||||
2016,1100.000000
|
||||
2017,1800.000000
|
||||
2018,2000.000000
|
||||
2019,2600.000000
|
||||
2020,2600.000000
|
||||
2021,3100.000000
|
||||
2022,3000.000000
|
||||
2023,2900.000000
|
||||
2024,2350.000000
|
||||
2025,1485.000000
|
||||
|
@@ -0,0 +1,21 @@
|
||||
period,output,yoy
|
||||
2006,397.100000,
|
||||
2007,493.900000,
|
||||
2008,569.300000,
|
||||
2009,706.900000,
|
||||
2010,890.800000,
|
||||
2011,1025.600000,
|
||||
2012,1153.200000,
|
||||
2013,1226.200000,
|
||||
2014,1257.100000,
|
||||
2015,1312.800000,
|
||||
2016,1358.400000,
|
||||
2017,1198.100000,
|
||||
2018,871.200000,
|
||||
2019,786.000000,
|
||||
2020,740.700000,
|
||||
2021,715.600000,
|
||||
2022,671.200000,
|
||||
2023,449.200000,
|
||||
2024,414.500000,
|
||||
2025,354.900000,
|
||||
|
@@ -0,0 +1,13 @@
|
||||
period,before_price,after_price,increase_rate
|
||||
2001-08,185.000000,218.000000,0.178000
|
||||
2003-10,218.000000,268.000000,0.229000
|
||||
2006-02,268.000000,308.000000,0.149000
|
||||
2007-03,308.000000,358.000000,0.162000
|
||||
2008-01,358.000000,439.000000,0.226000
|
||||
2010-01,439.000000,499.000000,0.137000
|
||||
2011-01,499.000000,619.000000,0.240000
|
||||
2012-09,619.000000,819.000000,0.323000
|
||||
2018-01,819.000000,969.000000,0.183000
|
||||
2023-11,969.000000,1169.000000,0.206000
|
||||
2026-03,1169.000000,1269.000000,0.086000
|
||||
2026-07,1269.000000,1369.000000,0.079000
|
||||
|
@@ -0,0 +1,11 @@
|
||||
period,moutai,series,moutai_share,series_share
|
||||
2016,367.140000,21.270000,0.945238,0.054762
|
||||
2017,523.940000,57.740000,0.900736,0.099264
|
||||
2018,654.870000,80.770000,0.890204,0.109796
|
||||
2019,758.020000,95.420000,0.888194,0.111806
|
||||
2020,848.310000,99.910000,0.894634,0.105366
|
||||
2021,934.650000,125.950000,0.881246,0.118754
|
||||
2022,1078.340000,159.390000,0.871224,0.128776
|
||||
2023,1265.890000,206.300000,0.859869,0.140131
|
||||
2024,1459.280760,246.837621,0.855322,0.144678
|
||||
2025,1464.999065,222.746787,0.868021,0.131979
|
||||
|
@@ -0,0 +1,22 @@
|
||||
period,revenue,gross_profit,net_profit,gross_margin,revenue_yoy,net_profit_yoy
|
||||
2005,39.310000,,11.190000,,,
|
||||
2006,48.960000,,15.040000,,0.245485,0.344057
|
||||
2007,72.370000,,28.310000,,0.478145,0.882314
|
||||
2008,82.420000,,37.990000,,0.138870,0.341929
|
||||
2009,96.700000,,43.120000,,0.173259,0.135036
|
||||
2010,116.330000,,50.510000,,0.202999,0.171382
|
||||
2011,184.020000,,87.630000,,0.581879,0.734904
|
||||
2012,264.550000,,133.080000,,0.437615,0.518658
|
||||
2013,309.220000,,151.370000,,0.168853,0.137436
|
||||
2014,315.740000,,153.500000,,0.021085,0.014071
|
||||
2015,326.600000,,155.030000,,0.034395,0.009967
|
||||
2016,388.620000,,167.180000,0.912300,0.189896,0.078372
|
||||
2017,582.180000,,270.790000,0.898000,0.498070,0.619751
|
||||
2018,736.390000,,352.040000,0.911400,0.264884,0.300048
|
||||
2019,854.300000,,412.060000,0.913000,0.160119,0.170492
|
||||
2020,949.150000,867.390000,466.970000,0.914100,0.111027,0.133257
|
||||
2021,1061.900000,972.070000,524.600000,0.915400,0.118790,0.123413
|
||||
2022,1241.000000,1140.060000,627.160000,0.918700,0.168660,0.195501
|
||||
2023,1476.940000,1358.260000,747.340000,0.919600,0.190121,0.191626
|
||||
2024,1708.990000,1571.100000,862.280000,0.919300,0.157115,0.153799
|
||||
2025,1688.380000,1539.460000,823.200000,0.911800,-0.012060,-0.045322
|
||||
|
@@ -0,0 +1,53 @@
|
||||
period,revenue,gross_profit,net_profit,gross_margin,revenue_yoy,net_profit_yoy
|
||||
2020Q1,244.050000,223.730000,130.940000,0.916700,,
|
||||
2020Q1,20.320000,,,,-0.916738,
|
||||
2020Q2,195.470000,178.280000,95.080000,0.912000,8.619587,
|
||||
2020Q2,17.200000,,,,-0.912007,
|
||||
2020Q3,232.620000,211.840000,112.250000,0.910600,12.524419,
|
||||
2020Q3,20.790000,,,,-0.910627,
|
||||
2020Q4,277.000000,253.770000,128.700000,0.916100,12.323713,
|
||||
2020Q4,23.230000,,,,-0.916137,
|
||||
2021Q1,272.710000,250.010000,139.540000,0.916800,10.739561,
|
||||
2021Q1,22.700000,,,,-0.916761,
|
||||
2021Q2,218.160000,198.540000,107.000000,0.910100,8.610573,
|
||||
2021Q2,19.620000,,,,-0.910066,
|
||||
2021Q3,255.550000,232.120000,126.120000,0.908300,12.024975,
|
||||
2021Q3,23.430000,,,,-0.908315,
|
||||
2021Q4,315.480000,291.400000,151.940000,0.923700,12.464789,
|
||||
2021Q4,24.080000,,,,-0.923672,
|
||||
2022Q1,322.960000,298.310000,172.450000,0.923700,12.411960,
|
||||
2022Q1,24.650000,,,,-0.923675,
|
||||
2022Q2,253.210000,232.400000,125.490000,0.917800,9.272211,
|
||||
2022Q2,20.810000,,,,-0.917815,
|
||||
2022Q3,295.430000,270.080000,146.060000,0.914200,13.196540,
|
||||
2022Q3,25.360000,,,,-0.914159,
|
||||
2022Q4,369.400000,339.280000,183.170000,0.918500,13.566246,
|
||||
2022Q4,30.110000,,,,-0.918489,
|
||||
2023Q1,387.560000,358.860000,207.950000,0.926000,11.871471,
|
||||
2023Q1,28.700000,,,,-0.925947,
|
||||
2023Q2,308.200000,279.830000,151.860000,0.908000,9.738676,
|
||||
2023Q2,28.370000,,,,-0.907949,
|
||||
2023Q3,336.920000,308.350000,168.960000,0.915200,10.875925,
|
||||
2023Q3,28.570000,,,,-0.915202,
|
||||
2023Q4,444.250000,411.220000,218.580000,0.925600,14.549527,
|
||||
2023Q4,33.030000,,,,-0.925650,
|
||||
2024Q1,457.760000,423.940000,240.650000,0.926100,12.858916,
|
||||
2024Q1,33.810000,,,,-0.926140,
|
||||
2024Q2,361.550000,327.860000,176.300000,0.906800,9.693582,
|
||||
2024Q2,33.700000,,,,-0.906790,
|
||||
2024Q3,388.450000,353.680000,191.320000,0.910500,10.526706,
|
||||
2024Q3,34.770000,,,,-0.910490,
|
||||
2024Q4,501.230000,465.620000,254.010000,0.928900,13.415588,
|
||||
2024Q4,35.610000,,,,-0.928955,
|
||||
2025Q1,506.010000,465.400000,268.470000,0.919700,13.209773,
|
||||
2025Q1,40.610000,,,,-0.919745,
|
||||
2025Q2,387.880000,350.720000,185.550000,0.904200,8.551342,
|
||||
2025Q2,37.160000,,,,-0.904197,
|
||||
2025Q3,390.640000,356.580000,192.240000,0.912800,9.512379,
|
||||
2025Q3,34.070000,,,,-0.912784,
|
||||
2025Q4,403.840000,366.760000,176.930000,0.908000,10.853243,
|
||||
2025Q4,37.080000,,,,-0.908181,
|
||||
2026Q1,539.090000,483.890000,272.430000,0.908000,13.538565,
|
||||
2026Q1,55.210000,,,,-0.897587,
|
||||
2026Q2,367.940000,328.400000,172.740000,,5.664372,
|
||||
2026Q2,39.530000,,,,-0.892564,
|
||||
|
@@ -0,0 +1,21 @@
|
||||
period,moutai,series,domestic,overseas
|
||||
2016H2,367.140000,21.270000,367.820000,20.590000
|
||||
2017H1,216.270000,25.490000,233.340000,8.420000
|
||||
2017H2,523.940000,57.740000,558.980000,22.700000
|
||||
2018H1,293.830000,39.930000,323.450000,10.310000
|
||||
2018H2,654.870000,80.770000,706.720000,28.930000
|
||||
2019H1,347.950000,46.550000,383.680000,10.810000
|
||||
2019H2,758.020000,95.420000,824.240000,29.200000
|
||||
2020H1,392.610000,46.500000,428.100000,11.010000
|
||||
2020H2,848.310000,99.910000,923.900000,24.320000
|
||||
2021H1,429.490000,60.600000,478.100000,12.000000
|
||||
2021H2,934.650000,125.950000,1034.410000,26.180000
|
||||
2022H1,499.650000,75.980000,554.970000,20.660000
|
||||
2022H2,1078.340000,159.390000,1195.330000,42.400000
|
||||
2023H1,592.790000,100.740000,673.110000,20.420000
|
||||
2023H2,1265.890000,206.300000,1428.690000,43.500000
|
||||
2024H1,685.670000,131.470000,797.180000,22.130000
|
||||
2024H2,1459.280000,246.840000,1654.230000,51.890000
|
||||
2025H1,755.900000,137.630000,864.900000,28.990000
|
||||
2025H2,1465.000000,222.750000,1639.240000,48.500000
|
||||
2026H1,777.240000,129.340000,896.660000,10.370000
|
||||
|
@@ -0,0 +1,9 @@
|
||||
period,moutai_revenue,series_revenue,moutai_volume,series_volume,moutai_price_per_ton,series_price_per_ton
|
||||
2016,367.140000,21.270000,22918.000000,14027.000000,160.197225,15.163613
|
||||
2017,523.940000,57.740000,30206.000000,29903.000000,173.455605,19.309099
|
||||
2018,654.870000,80.770000,32464.000000,29774.000000,201.721907,27.127695
|
||||
2019,758.020000,95.420000,34562.000000,30083.000000,219.321799,31.718911
|
||||
2020,848.310000,99.910000,34313.000000,29743.000000,247.226999,33.591097
|
||||
2021,934.650000,125.950000,36261.000000,30177.000000,257.756267,41.737085
|
||||
2022,1078.340000,159.390000,37901.000000,30275.000000,284.514920,52.647399
|
||||
2023,1265.890000,206.300000,42111.000000,31165.000000,300.607917,66.196053
|
||||
|
@@ -0,0 +1,5 @@
|
||||
# 贵州茅台图表数据
|
||||
|
||||
本目录保留原始附件:年度与季度工作簿均不作编辑。图表使用的 `data/moutai/*.csv` 由 `scripts/normalize_moutai_financials.py` 生成。
|
||||
|
||||
口径说明:金额默认单位为人民币亿元;季度现金流由各披露累计值与同年度上一季度累计值相减得到单季金额;“分类营收”中的单季与累计数据分别保留,图表不混用两种口径。
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,8 @@
|
||||
period,retail,robot_store,own_app,tiktok,amazon,online_other,wholesale,total_revenue
|
||||
2024H1,0.9,0.1,0.3,0.1,0.0,0.2,0.2,1.83
|
||||
2024H2,2.5,0.3,1.6,0.9,0.0,0.5,0.4,6.19
|
||||
2024FY,3.4,0.4,1.9,1.1,0.0,0.7,0.6,8.02
|
||||
2025H1,7.4,1.1,8.9,3.1,0.0,1.3,1.0,22.65
|
||||
2025H2,12.7,1.3,19.8,7.3,0.0,3.2,1.2,45.42
|
||||
2025FY,20.0,2.3,28.7,10.4,0.0,4.4,2.2,68.07
|
||||
2026H1,9.0,1.0,4.9,0.0,1.7,0.6,1.6,18.91
|
||||
|
@@ -0,0 +1,3 @@
|
||||
period,retail,robot_store,own_app,tiktok,amazon,online_other,wholesale,total_revenue
|
||||
2024FY,3.4,0.4,1.9,1.1,0.0,0.7,0.6,8.02
|
||||
2025FY,20.0,2.3,28.7,10.4,0.0,4.4,2.2,68.07
|
||||
|
@@ -0,0 +1,6 @@
|
||||
period,retail,robot_store,own_app,tiktok,amazon,online_other,wholesale,total_revenue
|
||||
2024H1,0.9,0.1,0.3,0.1,0.0,0.2,0.2,1.83
|
||||
2024H2,2.5,0.3,1.6,0.9,0.0,0.5,0.4,6.19
|
||||
2025H1,7.4,1.1,8.9,3.1,0.0,1.3,1.0,22.65
|
||||
2025H2,12.7,1.3,19.8,7.3,0.0,3.2,1.2,45.42
|
||||
2026H1,9.0,1.0,4.9,0.0,1.7,0.6,1.6,18.91
|
||||
|
@@ -0,0 +1,8 @@
|
||||
period,retail,robot_store,own_app,shopee,online_other,wholesale,total_revenue
|
||||
2024H1,4.8,0.2,0.4,0.4,0.8,1.3,7.97
|
||||
2024H2,11.8,0.2,2.6,2.4,3.0,3.0,23.13
|
||||
2024FY,16.7,0.4,3.0,2.9,3.8,4.3,31.10
|
||||
2025H1,14.9,0.3,3.1,2.8,4.8,2.6,28.51
|
||||
2025H2,28.3,0.6,11.4,2.9,5.5,2.9,51.60
|
||||
2025FY,43.2,0.9,14.6,5.7,10.3,5.4,80.11
|
||||
2026H1,17.2,0.6,3.4,1.1,2.0,1.6,25.76
|
||||
|
@@ -0,0 +1,3 @@
|
||||
period,retail,robot_store,own_app,shopee,online_other,wholesale,total_revenue
|
||||
2024FY,16.7,0.4,3.0,2.9,3.8,4.3,31.10
|
||||
2025FY,43.2,0.9,14.6,5.7,10.3,5.4,80.11
|
||||
|
@@ -0,0 +1,6 @@
|
||||
period,retail,robot_store,own_app,shopee,online_other,wholesale,total_revenue
|
||||
2024H1,4.8,0.2,0.4,0.4,0.8,1.3,7.97
|
||||
2024H2,11.8,0.2,2.6,2.4,3.0,3.0,23.13
|
||||
2025H1,14.9,0.3,3.1,2.8,4.8,2.6,28.51
|
||||
2025H2,28.3,0.6,11.4,2.9,5.5,2.9,51.60
|
||||
2026H1,17.2,0.6,3.4,1.1,2.0,1.6,25.76
|
||||
|
@@ -0,0 +1,8 @@
|
||||
period,retail,robot_store,roboshop,tmall,douyin,online_other,wholesale,total_revenue
|
||||
2024H1,20.0,3.4,4.0,2.0,2.1,1.3,2.4,35.22
|
||||
2024H2,26.0,4.1,7.2,4.3,3.9,3.0,5.2,53.65
|
||||
2024FY,46.0,7.5,11.1,6.3,6.0,4.3,7.6,88.87
|
||||
2025H1,44.1,6.8,11.3,6.6,5.6,5.9,2.6,82.82
|
||||
2025H2,56.7,6.7,23.0,11.3,10.3,11.3,6.5,125.69
|
||||
2025FY,100.8,13.5,34.2,17.9,15.9,17.2,9.1,208.51
|
||||
2026H1,60.9,7.8,20.6,9.0,9.8,8.4,5.5,122.00
|
||||
|
@@ -0,0 +1,3 @@
|
||||
period,retail,robot_store,roboshop,tmall,douyin,online_other,wholesale,total_revenue
|
||||
2024FY,46.0,7.5,11.1,6.3,6.0,4.3,7.6,88.87
|
||||
2025FY,100.8,13.5,34.2,17.9,15.9,17.2,9.1,208.51
|
||||
|
@@ -0,0 +1,6 @@
|
||||
period,retail,robot_store,roboshop,tmall,douyin,online_other,wholesale,total_revenue
|
||||
2024H1,20.0,3.4,4.0,2.0,2.1,1.3,2.4,35.22
|
||||
2024H2,26.0,4.1,7.2,4.3,3.9,3.0,5.2,53.65
|
||||
2025H1,44.1,6.8,11.3,6.6,5.6,5.9,2.6,82.82
|
||||
2025H2,56.7,6.7,23.0,11.3,10.3,11.3,6.5,125.69
|
||||
2026H1,60.9,7.8,20.6,9.0,9.8,8.4,5.5,122.00
|
||||
|
@@ -0,0 +1,8 @@
|
||||
period,retail,robot_store,own_app,online_other,wholesale,total_revenue
|
||||
2024H1,0.4,0.0,0.0,0.1,0.1,0.58
|
||||
2024H2,1.2,0.0,0.2,0.2,0.2,1.81
|
||||
2024FY,1.6,0.0,0.2,0.3,0.3,2.39
|
||||
2025H1,2.7,0.0,0.8,0.8,0.4,4.77
|
||||
2025H2,5.8,0.1,2.3,1.1,0.4,9.74
|
||||
2025FY,8.6,0.2,3.2,1.9,0.7,14.51
|
||||
2026H1,4.0,0.1,0.5,0.1,0.2,5.06
|
||||
|
@@ -0,0 +1,3 @@
|
||||
period,retail,robot_store,own_app,online_other,wholesale,total_revenue
|
||||
2024FY,1.6,0.0,0.2,0.3,0.3,2.39
|
||||
2025FY,8.6,0.2,3.2,1.9,0.7,14.51
|
||||
|
@@ -0,0 +1,6 @@
|
||||
period,retail,robot_store,own_app,online_other,wholesale,total_revenue
|
||||
2024H1,0.4,0.0,0.0,0.1,0.1,0.58
|
||||
2024H2,1.2,0.0,0.2,0.2,0.2,1.81
|
||||
2025H1,2.7,0.0,0.8,0.8,0.4,4.77
|
||||
2025H2,5.8,0.1,2.3,1.1,0.4,9.74
|
||||
2026H1,4.0,0.1,0.5,0.1,0.2,5.06
|
||||
|
@@ -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
|
||||
|
@@ -1,16 +0,0 @@
|
||||
region,channel,value
|
||||
中国,线下渠道,68.69
|
||||
中国,线上渠道,47.79
|
||||
中国,批发及其他,5.53
|
||||
海外,线下渠道,31.94
|
||||
海外,线上渠道,14.34
|
||||
海外,批发及其他,3.45
|
||||
亚太,线下渠道,17.73
|
||||
亚太,线上渠道,6.44
|
||||
亚太,批发及其他,1.59
|
||||
美洲,线下渠道,10.07
|
||||
美洲,线上渠道,7.22
|
||||
美洲,批发及其他,1.62
|
||||
欧洲及其他,线下渠道,4.14
|
||||
欧洲及其他,线上渠道,0.68
|
||||
欧洲及其他,批发及其他,0.24
|
||||
|
@@ -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
|
||||
|
||||
|
@@ -0,0 +1,189 @@
|
||||
# Smart Fin Chart 视觉与图表风格规范
|
||||
|
||||
> 用途:把本项目的页面和图表复刻成同一种视觉语言,或把本规范直接交给其他 AI 生成新页面、图表或 ECharts 配置。
|
||||
>
|
||||
> 本文以现有实现为准:`css/style.css`、`js/theme.js` 与各图表配置。它不替代数据口径或交互需求。
|
||||
|
||||
## 1. 风格定位
|
||||
|
||||
**正式主题名:轻快消费(Light Consumer Finance)**。它建立在“Joey 风格”的克制财经媒体底子上:
|
||||
|
||||
- 页面:暖灰白底、白色内容卡、深棕作为品牌锚点,安静、清爽、有纸张感;
|
||||
- 图表:低饱和莫兰迪基础色,叠加清晰但不荧光的青绿、蓝、琥珀、珊瑚与紫;
|
||||
- 气质:专业、可亲近、信息密度高但不冰冷。更像编辑过的消费公司研究报告,而不是交易终端、科技 SaaS 或奢侈品画册;
|
||||
- 色彩策略:**页面保持中性,数据才使用颜色;一张图只设置一个视觉主角。**
|
||||
|
||||
不要把它误做成两种极端:不是厚重老钱/深棕复古风,也不是高饱和糖果色的消费品牌海报。
|
||||
|
||||
## 2. 设计令牌(直接复用)
|
||||
|
||||
### 页面基础色
|
||||
|
||||
| 角色 | 色值 | 用法 |
|
||||
| --- | --- | --- |
|
||||
| 页面背景 | `#F5F4F1` | 大面积暖灰白,不用纯白铺满全页 |
|
||||
| 内容表面 | `#FFFFFF` | 图表卡片、弹窗、选中态 |
|
||||
| 主文字 | `#333333` | 标题、正文、坐标轴重点文字 |
|
||||
| 次级文字 | `#8C8C8C` | 副标题、数据来源、脚注、弱提示 |
|
||||
| 分隔/网格 | `#E8E8E8` | 卡片分割线、图表横向网格 |
|
||||
| 坐标轴线 | `#D0D0D0` | 仅分类横轴等必要位置 |
|
||||
| 品牌强调/交互 | `#6B4A2B` | 选中 Tab、主要按钮、hover 文字 |
|
||||
| 品牌浅底 | `#F0E9E1` | 选中 Tab、筛选 chip、浅强调块 |
|
||||
|
||||
### 通用莫兰迪扩展色
|
||||
|
||||
用于非固定财务语义的分类图、桑基图或适量的次级系列。它们是备用色,不是要求一张图全用完。
|
||||
|
||||
| 色名 | 色值 | 视觉感觉 |
|
||||
| --- | --- | --- |
|
||||
| 棕 | `#6B4A2B` | 温暖、稳定、品牌锚点 |
|
||||
| 沙 | `#D5C7B4` | 最弱的暖中性色 |
|
||||
| 珊瑚 | `#EC7B73` | 柔和警示或重点 |
|
||||
| 绿 | `#2F8A5B` | 稳健正向 |
|
||||
| 橙 | `#D98324` | 温暖重点 |
|
||||
| 紫 | `#9B4FA8` | 指标/同比强调 |
|
||||
| 蓝 | `#3B6EA5` | 常规数据类别 |
|
||||
| 海军蓝 | `#1F4E79` | 深层级、强对比类别 |
|
||||
| 红 | `#B8342A` | 显著负向/风险,慎用 |
|
||||
| 鼠尾草绿 | `#8AAB7E` | 柔和的正向或过渡类别 |
|
||||
|
||||
## 3. 图表语义色(优先级高于通用色)
|
||||
|
||||
同一业务语义跨图必须保持一致;不要在单张图内重写近似颜色。
|
||||
|
||||
### 财务与经营指标
|
||||
|
||||
| 语义 | 色值 | 推荐图形角色 |
|
||||
| --- | --- | --- |
|
||||
| 经营现金流/正向贡献 | `#36A28F` | 柱、瀑布正向项 |
|
||||
| 投资现金流/基准值/现金 | `#4D83D1` | 柱、瀑布起点和终点 |
|
||||
| 筹资现金流/负债 | `#EEA64F` | 柱 |
|
||||
| 现金净增加/关键压力项/负向贡献 | `#D95567` | 高辨识度折线、瀑布负向项 |
|
||||
| 核心/净利润 | `#F08B5A` | 主柱 |
|
||||
| 同比、转化率等唯一强调指标 | `#8256B2` | 主折线 |
|
||||
|
||||
**瀑布图固定规则:**总计/基准 `#4D83D1`,正向 `#36A28F`,负向 `#D95567`。透明占位柱不着色。
|
||||
|
||||
### 渠道业务
|
||||
|
||||
| 语义 | 色值 | 推荐图形角色 |
|
||||
| --- | --- | --- |
|
||||
| 零售店 | `#3F7FBE` | 堆叠柱、区域/渠道分类 |
|
||||
| 线上 | `#E36D63` | 堆叠柱、渠道分类 |
|
||||
| 机器人商店 | `#71AD81` | 堆叠柱、渠道分类 |
|
||||
| 批发 | `#E4A349` | 堆叠柱、渠道分类 |
|
||||
| 营收增速 | `#8256B2` | 柱线组合中的唯一折线 |
|
||||
|
||||
### 多类别 IP/占比图(10+ 类)
|
||||
|
||||
按色相顺序使用,残差项始终为中性灰:
|
||||
|
||||
`#D64545` → `#E8913C` → `#D9C043` → `#7FA83C` → `#3E9E6E` → `#3FB6D0` → `#2E5FA8` → `#8E86E0` → `#8A3FA8` → `#D081C4` → `#8C8C8C`
|
||||
|
||||
规则:红—橙—金—橄榄—翡翠—青—蓝—紫的连续色相谱;冷色相邻时交替深浅;“其他”“外采”“未单列”等残差类别固定用灰。该谱只用于高类别数的占比/构成图,不应用于普通两三系列柱线图。
|
||||
|
||||
## 4. 页面样式语言
|
||||
|
||||
### 布局和容器
|
||||
|
||||
- 外层是暖灰白画布,内容居中,桌面端采用“窄侧栏 + 主内容 + 窄目录”的研究报告式三栏;
|
||||
- 白色图表卡片圆角 `10px`,仅有极浅阴影 `0 1px 3px rgba(0,0,0,.06)`,不使用厚重浮层;
|
||||
- 卡片之间留出约 `28px` 空白。卡片顶部工具栏高度约 `52px`,下方用 `#E8E8E8` 细线分隔;
|
||||
- 侧栏和 Tab 容器为半透明白(约 58%–60%),一像素浅边框、`9–10px` 圆角;移动端转为贴顶的半透明暖灰条,并带轻微 backdrop blur;
|
||||
- 主操作按钮为深棕底白字;次操作为白底浅灰边框,hover 时变深棕字/边。选中态使用深棕字加 `#F0E9E1` 浅底,不要大面积实色块。
|
||||
|
||||
### 字体与文字层级
|
||||
|
||||
- 字体:`PingFang SC, Hiragino Sans GB, Microsoft YaHei, sans-serif`;
|
||||
- 页面卡片标题:`14px / 600`;侧栏标题:`12px / 600`;来源文件:`11px` 等宽字体;
|
||||
- 图表主标题:居中、`19px / bold`、`#333333`;副标题:`13px`、`#8C8C8C`、`line-height: 20px`;
|
||||
- 图例:`14px`、`#333333`;坐标轴文字:重点分类 `14px`,数值刻度 `13px`;数据标签通常 `11–13px`;脚注 `12px`、灰色;
|
||||
- 文案不使用全大写、超粗黑体或夸张字号。层级靠字重、灰度、间距建立,不靠多种彩色文字。
|
||||
|
||||
## 5. 所有图表共享的画法
|
||||
|
||||
### 留白、坐标轴与网格
|
||||
|
||||
- 图表白底;标题顶边距约 `18px`,图例在约 `85px`,绘图区从约 `145px` 开始,形成稳定的“标题 → 副标题 → 图例 → 数据”节奏;
|
||||
- 数值轴隐藏轴线和刻度,仅保留 `#E8E8E8`、`1px` 的浅色横向网格;分类横轴可保留 `#D0D0D0` 细轴线,但不显示刻度;
|
||||
- 不做渐变背景、斑马条纹(矩阵/热力图除外)、3D、强阴影或粗黑边框;
|
||||
- 图表不依赖 hover 才展示关键数据:数据标签默认可见;密集处通过上下错位、外置、白色描边和避让解决。
|
||||
|
||||
### 图例、标签与 tooltip
|
||||
|
||||
- 图例优先置于副标题和绘图区之间,横排居中,项目间距约 `28px`;让图表库按类型生成图例图标:柱=矩形,线=短线+空心圆;
|
||||
- 折线:不平滑(直线),线宽通常 `3px`;白心圆点,直径 `9px`,彩色描边 `2.5px`;标签同线色,必要时加 `3px` 白描边;
|
||||
- 柱:无渐变、无厚边框、圆角克制或不加;主图中柱宽按密度保持适中;柱顶 `11px` 深灰数值标签。很矮的柱也尽量显示标签;
|
||||
- 堆叠占比:足够宽的段内白字/深字;过窄段把标签置于右侧,文字变深灰并加白描边;
|
||||
- tooltip 使用系统默认浅底即可,字体遵循中文无衬线;指示器用 shadow/cross,不渲染为夸张的彩色悬浮卡;
|
||||
- 每张图底部保留灰色脚注,解释口径与来源。背景可铺低干扰水印:深暖灰 `rgba(120,100,80,.10)`、`20px bold`、约 `20°` 斜向、300×200px 网格重复。
|
||||
|
||||
## 6. 关键图表配方
|
||||
|
||||
| 图形 | 推荐结构 | 色彩与样式规则 |
|
||||
| --- | --- | --- |
|
||||
| 现金流/利润 + 同比 | 多柱 + 单条双轴折线 | 柱用固定财务色;只保留一条紫色或珊瑚色重点折线;折线置顶,白心圆点、`3px` 线、显示标签 |
|
||||
| 渠道收入结构 + 增速 | 堆叠柱 + 单条折线 | 渠道色按零售蓝、线上珊瑚、机器人绿、批发琥珀固定;增速只用紫线;不要再引入第二条抢眼折线 |
|
||||
| 瀑布图/同比拆解 | 蓝色起终点 + 绿/红贡献柱 | 正负值同时显示 `+/-` 标签;透明占位列;分类横轴可斜 32°,其余保持克制 |
|
||||
| 多条经营比率线 | 4 条以内为宜 | 选深青、芥黄、茄紫、玫红等明度与色相均有区分的低饱和色;靠实/虚线、线宽、标签上下位置进一步区分 |
|
||||
| IP 占比饼图 | 非环图 + 两侧外标签 + 右侧纵向图例 | 扇区之间 `2px` 白边;名称按扇区同色,数值灰色;右侧图例使用“名称 + 次行数值/占比”;hover 阴影轻微 |
|
||||
| 100% 堆叠/区域份额 | 归一化堆叠柱 | 颜色固定映射业务类别,不能随年份换色;小段标签外置并避让;不要因标签多删去数据 |
|
||||
| 热力图 | 低增长蓝 → 暖白 → 高增长珊瑚 | 色阶为 `#9CC4D0 → #F3EEE6 → #EC7B73`;格中深灰数值;水平色尺说明高低;缺失值留白 |
|
||||
| 桑基图 | 低饱和节点 + 平滑渐变流向 | 节点取主题分类色,连线取源到目标的渐变;文字深灰、重点节点可用珊瑚/绿色,避免彩虹化 |
|
||||
|
||||
## 7. 配色决策规则
|
||||
|
||||
1. 先判断语义:财务、渠道、瀑布先使用固定语义色;只有无固定业务含义的分类才从扩展色选。
|
||||
2. 一图只有一个主角:柱负责区分类别,折线只留一个高辨识度强调色。避免红、橙、紫同时作为抢眼元素。
|
||||
3. 颜色不是唯一编码:折线还用线宽、实虚线、白心圆点、标签优先级区分;不要仅依赖色差。
|
||||
4. 2–4 个系列人工挑色即可;5–9 个系列保证相邻/同图的色相与明度明显不同;10+ 类使用预设色相谱。不要为全页所有图强制颜色互不重复。
|
||||
5. “其他、未披露、残差、外采”优先灰色/沙色;负向或风险优先珊瑚红,普通分类不要随意占用红色。
|
||||
|
||||
## 8. 可直接交给 AI 的提示词
|
||||
|
||||
将方括号内容替换成具体业务。若目标 AI 能生成代码,也可要求它输出 CSS 变量和 ECharts/图表库配置。
|
||||
|
||||
```text
|
||||
请设计一个中文消费公司财报数据页面,视觉风格为“轻快消费(Light Consumer Finance)”:
|
||||
以克制财经媒体/编辑研究报告为底子,而非交易终端或科技 SaaS。页面用暖灰白 #F5F4F1 背景、白色 #FFFFFF 图表卡片、10px 圆角、极浅阴影;主文字 #333333,辅助文字 #8C8C8C,细分隔和网格 #E8E8E8。品牌和选中态只使用深棕 #6B4A2B,浅强调底 #F0E9E1。
|
||||
|
||||
中文字体使用 PingFang SC / Hiragino Sans GB / Microsoft YaHei。图表标题居中、19px bold;副标题 13px 灰;图例 14px;数值轴浅灰横网格、弱化轴线和刻度。保留充足留白,画面安静、专业、可亲近。
|
||||
|
||||
为 [图表主题] 生成 [图表类型]。关键数据默认显示,不依赖 hover:柱顶显示深灰数字;折线是直线、3px、9px 白心彩边圆点,并显示同色标签(必要时白色描边)。图例放在副标题和绘图区之间,柱图例为矩形、线图例为短线加空心圆。
|
||||
|
||||
按固定业务语义配色:[在此选择财务色或渠道色]。一张图只保留一个视觉主角;柱承载分类,最多一条高辨识度折线。禁止霓虹色、渐变填充、3D 图表、粗黑边框、厚阴影、深色背景、过多鲜艳色和无意义装饰。
|
||||
|
||||
卡片底部添加简短灰色脚注说明数据口径和来源;可选地铺很淡的 20° 斜向暖灰水印,但绝不影响读数。
|
||||
```
|
||||
|
||||
### 财务色替换段
|
||||
|
||||
```text
|
||||
财务语义固定为:经营现金流/正向贡献 #36A28F,投资现金流/基准/现金 #4D83D1,筹资现金流/负债 #EEA64F,现金净增加/关键压力/负向贡献 #D95567,净利润 #F08B5A,同比或转化率的唯一强调折线 #8256B2。瀑布图总计蓝、正向青绿、负向珊瑚红。
|
||||
```
|
||||
|
||||
### 渠道色替换段
|
||||
|
||||
```text
|
||||
渠道语义固定为:零售店 #3F7FBE,线上 #E36D63,机器人商店 #71AD81,批发 #E4A349;若有营收增速,仅用一条 #8256B2 紫色折线。跨年度或跨图保持同一渠道不换色。
|
||||
```
|
||||
|
||||
## 9. 实施检查清单
|
||||
|
||||
- [ ] 页面背景不是纯白,卡片是白色且阴影很淡。
|
||||
- [ ] 主文字/次文字/网格分别使用深灰、灰色、浅灰,品牌深棕没有滥用。
|
||||
- [ ] 图表使用业务语义色,且同一语义跨图一致。
|
||||
- [ ] 一张图没有多个同时抢眼的红橙紫强调系列。
|
||||
- [ ] 折线为直线、白心圆点;柱和折线的图例图标能区分。
|
||||
- [ ] 网格以浅色横线为主,轴线、边框和阴影都很轻。
|
||||
- [ ] 标签、图例、脚注不重叠;很小的堆叠段有可读的外置标签。
|
||||
- [ ] 视觉表达是“轻快、克制的消费公司研究报告”,不是金融终端或营销海报。
|
||||
|
||||
## 10. 来源与维护
|
||||
|
||||
- 页面令牌:`css/style.css`
|
||||
- 通用色板、字体、坐标轴、图例、系列样式、水印:`js/theme.js`
|
||||
- 正式方向与使用规则:`CLAUDE.md` 的“配色”部分、`docs/交接说明_2026-08-28.md`
|
||||
- 关键图表示例:`js/charts/ppmt_cashflow_composition.js`、`ppmt_core_profit.js`、`ppmt_bridge.js`、`ppmt_ip_share.js`、`ppmt_region_channel_yoy_timeseries.js`
|
||||
|
||||
如果修改正式色板,应同步更新 `js/theme.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 = [`<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知行记'),
|
||||
],
|
||||
};
|
||||
}
|
||||
+1
-1
@@ -72,7 +72,7 @@ export function growthChart(rows, { width, height }) {
|
||||
],
|
||||
graphic: [
|
||||
...watermarks(width, height),
|
||||
footnote('示例数据 | @思考的Joey'),
|
||||
footnote('示例数据 | @Joey知行记'),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
+45
-2
@@ -8,7 +8,6 @@ import { profitChart } from './ppmt_profit.js';
|
||||
import { ipChart } from './ppmt_ip.js';
|
||||
import { categoryChart } from './ppmt_category.js';
|
||||
import { sankeyChart } from './ppmt_sankey.js';
|
||||
import { regionChannelChart } from './ppmt_region_channel.js';
|
||||
import { regionChannelYoyChart } from './ppmt_region_yoy.js';
|
||||
import { regionChannelYoyTimeseriesChart } from './ppmt_region_channel_yoy_timeseries.js';
|
||||
import { ppmtGrowthChart } from './ppmt_growth.js';
|
||||
@@ -26,6 +25,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, makeRegionChannelAmountChart } 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 +33,19 @@ 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';
|
||||
import {
|
||||
moutaiProfitabilityAnnualChart, moutaiProfitabilityQuarterlyChart,
|
||||
moutaiExpenseAnnualChart, moutaiExpenseQuarterlyChart,
|
||||
moutaiBalanceAnnualChart, moutaiBalanceQuarterlyChart,
|
||||
moutaiCashflowAnnualChart, moutaiCashflowQuarterlyChart,
|
||||
moutaiProductAnnualChart, moutaiChannelAnnualChart,
|
||||
moutaiClassifiedProductQuarterlyChart, moutaiClassifiedChannelQuarterlyChart,
|
||||
moutaiClassifiedProductCumulativeChart, moutaiClassifiedChannelCumulativeChart,
|
||||
moutaiRevenueCompositionHalfyearChart, moutaiLiquorIndustryOutputChart,
|
||||
moutaiBaseWineVolumeChart, moutaiFlyingPriceChart, moutaiPriceAdjustmentChart,
|
||||
moutaiTonPriceChart, moutaiDividendChart, moutaiDistributorsChart,
|
||||
} from './moutai.js';
|
||||
|
||||
/** 图表构建函数注册表。manifest 只引用这里的 key,不直接引用 JS 函数。 */
|
||||
export const chartRegistry = {
|
||||
@@ -46,7 +59,6 @@ export const chartRegistry = {
|
||||
'ppmt-ip': ipChart,
|
||||
'ppmt-category': categoryChart,
|
||||
'ppmt-sankey': sankeyChart,
|
||||
'ppmt-region-channel': regionChannelChart,
|
||||
'ppmt-region-yoy': regionChannelYoyChart,
|
||||
'ppmt-region-yoy-timeseries': regionChannelYoyTimeseriesChart,
|
||||
'ppmt-growth': ppmtGrowthChart,
|
||||
@@ -64,6 +76,14 @@ 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-channel-amount-china': makeRegionChannelAmountChart('china'),
|
||||
'ppmt-channel-amount-apac': makeRegionChannelAmountChart('apac'),
|
||||
'ppmt-channel-amount-americas': makeRegionChannelAmountChart('americas'),
|
||||
'ppmt-channel-amount-europe': makeRegionChannelAmountChart('europe'),
|
||||
'ppmt-stores-retail': storesRetailChart,
|
||||
'ppmt-stores-robot': storesRobotChart,
|
||||
'ppmt-region-share': regionShareChart,
|
||||
@@ -71,6 +91,29 @@ export const chartRegistry = {
|
||||
'ppmt-stores-retail-share': storesRetailShareChart,
|
||||
'ppmt-retail-store-revenue': retailStoreRevenueChart,
|
||||
'ppmt-robot-store-revenue': robotStoreRevenueChart,
|
||||
'brk-holdings': brkHoldingsChart,
|
||||
'moutai-profitability-annual': moutaiProfitabilityAnnualChart,
|
||||
'moutai-profitability-quarterly': moutaiProfitabilityQuarterlyChart,
|
||||
'moutai-expense-annual': moutaiExpenseAnnualChart,
|
||||
'moutai-expense-quarterly': moutaiExpenseQuarterlyChart,
|
||||
'moutai-balance-annual': moutaiBalanceAnnualChart,
|
||||
'moutai-balance-quarterly': moutaiBalanceQuarterlyChart,
|
||||
'moutai-cashflow-annual': moutaiCashflowAnnualChart,
|
||||
'moutai-cashflow-quarterly': moutaiCashflowQuarterlyChart,
|
||||
'moutai-product-annual': moutaiProductAnnualChart,
|
||||
'moutai-channel-annual': moutaiChannelAnnualChart,
|
||||
'moutai-classified-product-quarterly': moutaiClassifiedProductQuarterlyChart,
|
||||
'moutai-classified-channel-quarterly': moutaiClassifiedChannelQuarterlyChart,
|
||||
'moutai-classified-product-cumulative': moutaiClassifiedProductCumulativeChart,
|
||||
'moutai-classified-channel-cumulative': moutaiClassifiedChannelCumulativeChart,
|
||||
'moutai-revenue-composition-halfyear': moutaiRevenueCompositionHalfyearChart,
|
||||
'moutai-liquor-industry-output': moutaiLiquorIndustryOutputChart,
|
||||
'moutai-base-wine-volume': moutaiBaseWineVolumeChart,
|
||||
'moutai-flying-price': moutaiFlyingPriceChart,
|
||||
'moutai-price-adjustment': moutaiPriceAdjustmentChart,
|
||||
'moutai-ton-price': moutaiTonPriceChart,
|
||||
'moutai-dividend': moutaiDividendChart,
|
||||
'moutai-distributors': moutaiDistributorsChart,
|
||||
};
|
||||
|
||||
export function getChartBuilder(key) {
|
||||
|
||||
@@ -103,7 +103,7 @@ export function inventoryChart(rows, { width, height }) {
|
||||
...watermarks(width, height),
|
||||
footnote(
|
||||
'周转天数=平均存货÷当期销售成本×期间天数(半年181/年度365,期初=上一年末,日历天数口径);' +
|
||||
'与公司披露一致:23年133/24年102/25年123/26H1 201天 | @思考的Joey'
|
||||
'与公司披露一致:23年133/24年102/25年123/26H1 201天 | @Joey知行记'
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
+1
-1
@@ -74,7 +74,7 @@ export function marginChart(rows, { width, height }) {
|
||||
],
|
||||
graphic: [
|
||||
...watermarks(width, height),
|
||||
footnote('示例数据 | @思考的Joey'),
|
||||
footnote('示例数据 | @Joey知行记'),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,286 @@
|
||||
import { col } from '../csv.js';
|
||||
import {
|
||||
PALETTE, FONT, axisCommon, categoryAxis, titleStyle, standardLegend,
|
||||
lineSeries, footnote, watermarks, LAYOUT,
|
||||
} from '../theme.js';
|
||||
|
||||
const amountAxis = (name = '金额(亿元)', extra = {}) => ({
|
||||
...axisCommon,
|
||||
type: 'value',
|
||||
name,
|
||||
nameLocation: 'middle',
|
||||
nameGap: 52,
|
||||
nameTextStyle: { color: PALETTE.grey, fontSize: 13, fontFamily: FONT },
|
||||
...extra,
|
||||
});
|
||||
|
||||
const percentageAxis = (name = '比例(%)') => ({
|
||||
...amountAxis(name),
|
||||
axisLabel: { ...axisCommon.axisLabel, formatter: '{value}%' },
|
||||
splitLine: { show: false },
|
||||
});
|
||||
|
||||
const periodAxis = (rows) => ({
|
||||
...categoryAxis,
|
||||
data: col(rows, 'period'),
|
||||
axisLabel: {
|
||||
...categoryAxis.axisLabel,
|
||||
formatter: (value) => String(value).replace(/^20(\d{2})/, '$1'),
|
||||
},
|
||||
});
|
||||
|
||||
function option({ title, subtitle, rows, legend, series, yAxis, width, height, source }) {
|
||||
return {
|
||||
backgroundColor: '#fff',
|
||||
title: { ...titleStyle, text: title, subtext: subtitle },
|
||||
legend: standardLegend(legend),
|
||||
grid: { left: 78, right: 82, top: LAYOUT.gridTop, bottom: 90 },
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: { type: 'shadow' },
|
||||
textStyle: { fontFamily: FONT },
|
||||
valueFormatter: (value) => (Number.isFinite(value) ? value.toFixed(1) : '—'),
|
||||
},
|
||||
xAxis: periodAxis(rows),
|
||||
yAxis,
|
||||
series,
|
||||
graphic: [...watermarks(width, height), footnote(`数据来源:贵州茅台定期报告及附件底稿;${source} | @Joey知行记`)],
|
||||
};
|
||||
}
|
||||
|
||||
function bar(name, rows, key, color, extras = {}) {
|
||||
return {
|
||||
name,
|
||||
type: 'bar',
|
||||
data: col(rows, key),
|
||||
itemStyle: { color },
|
||||
emphasis: { focus: 'series' },
|
||||
...extras,
|
||||
};
|
||||
}
|
||||
|
||||
function profitChart(rows, { width, height, label }) {
|
||||
return option({
|
||||
title: `贵州茅台盈利能力(${label})`,
|
||||
subtitle: '营业收入、毛利、归母净利润与毛利率',
|
||||
rows,
|
||||
legend: ['营业收入', '毛利', '归母净利润', '毛利率'],
|
||||
yAxis: [amountAxis(), percentageAxis('毛利率(%)')],
|
||||
series: [
|
||||
bar('营业收入', rows, 'revenue', PALETTE.navy),
|
||||
bar('毛利', rows, 'gross_profit', PALETTE.sage),
|
||||
bar('归母净利润', rows, 'net_profit', PALETTE.orange),
|
||||
{ ...lineSeries({ name: '毛利率', data: col(rows, 'gross_margin').map((value) => (Number.isFinite(value) ? value * 100 : null)), color: PALETTE.red, formatter: (point) => `${point.value.toFixed(1)}%` }), yAxisIndex: 1, z: 10 },
|
||||
],
|
||||
width, height, source: `${label}口径;金额单位为亿元`,
|
||||
});
|
||||
}
|
||||
|
||||
export const moutaiProfitabilityAnnualChart = (rows, context) => profitChart(rows, { ...context, label: '年度' });
|
||||
export const moutaiProfitabilityQuarterlyChart = (rows, context) => profitChart(rows, { ...context, label: '单季' });
|
||||
|
||||
function expenseChart(rows, { width, height, label }) {
|
||||
const rates = rows.map((row) => {
|
||||
const value = (row.sales_expense_rate || 0) + (row.admin_expense_rate || 0) + (row.rd_expense_rate || 0);
|
||||
return Number.isFinite(value) ? value * 100 : null;
|
||||
});
|
||||
return option({
|
||||
title: `贵州茅台费用结构(${label})`,
|
||||
subtitle: '税金及附加单列展示;费用率为销售、管理和研发费用占营业收入比例',
|
||||
rows,
|
||||
legend: ['销售费用', '管理费用', '研发费用', '税金及附加', '三项费用率'],
|
||||
yAxis: [amountAxis(), percentageAxis('三项费用率(%)')],
|
||||
series: [
|
||||
bar('销售费用', rows, 'sales_expense', PALETTE.blue),
|
||||
bar('管理费用', rows, 'admin_expense', PALETTE.orange),
|
||||
bar('研发费用', rows, 'rd_expense', PALETTE.purple),
|
||||
bar('税金及附加', rows, 'tax_surcharge', PALETTE.sage),
|
||||
{ ...lineSeries({ name: '三项费用率', data: rates, color: PALETTE.red, formatter: (point) => `${point.value.toFixed(1)}%` }), yAxisIndex: 1, z: 10 },
|
||||
],
|
||||
width, height, source: `${label}口径;金额单位为亿元`,
|
||||
});
|
||||
}
|
||||
|
||||
export const moutaiExpenseAnnualChart = (rows, context) => expenseChart(rows, { ...context, label: '年度' });
|
||||
export const moutaiExpenseQuarterlyChart = (rows, context) => expenseChart(rows, { ...context, label: '单季' });
|
||||
|
||||
function balanceChart(rows, { width, height, label }) {
|
||||
return option({
|
||||
title: `贵州茅台资产负债结构(${label}期末)`,
|
||||
subtitle: '资产总计、所有者权益、负债与类现金余额',
|
||||
rows,
|
||||
legend: ['资产总计', '所有者权益', '负债合计', '类现金'],
|
||||
yAxis: amountAxis('期末余额(亿元)'),
|
||||
series: [
|
||||
bar('资产总计', rows, 'total_assets', PALETTE.navy),
|
||||
bar('所有者权益', rows, 'equity', PALETTE.sage),
|
||||
bar('负债合计', rows, 'liabilities', PALETTE.orange),
|
||||
{ ...lineSeries({ name: '类现金', data: col(rows, 'cash_like'), color: PALETTE.red, formatter: (point) => point.value.toFixed(0) }), z: 10 },
|
||||
],
|
||||
width, height, source: '资产负债表;金额单位为亿元',
|
||||
});
|
||||
}
|
||||
|
||||
export const moutaiBalanceAnnualChart = (rows, context) => balanceChart(rows, { ...context, label: '年度' });
|
||||
export const moutaiBalanceQuarterlyChart = (rows, context) => balanceChart(rows, { ...context, label: '季度' });
|
||||
|
||||
function cashflowChart(rows, { width, height, label }) {
|
||||
return option({
|
||||
title: `贵州茅台三大现金流(${label})`,
|
||||
subtitle: label === '单季' ? '季度单期值由披露的累计现金流量表相邻期间相减得出' : '年度现金流量表披露值',
|
||||
rows,
|
||||
legend: ['经营现金流', '投资现金流', '筹资现金流', '现金净增加'],
|
||||
yAxis: amountAxis('现金流量(亿元)'),
|
||||
series: [
|
||||
bar('经营现金流', rows, 'operating_cash_flow', '#36A28F'),
|
||||
bar('投资现金流', rows, 'investing_cash_flow', '#4D83D1'),
|
||||
bar('筹资现金流', rows, 'financing_cash_flow', '#EEA64F'),
|
||||
{ ...lineSeries({ name: '现金净增加', data: col(rows, 'net_cash_increase'), color: '#D95567', formatter: (point) => point.value.toFixed(0) }), z: 10 },
|
||||
],
|
||||
width, height, source: `${label}口径;金额单位为亿元`,
|
||||
});
|
||||
}
|
||||
|
||||
export const moutaiCashflowAnnualChart = (rows, context) => cashflowChart(rows, { ...context, label: '年度' });
|
||||
export const moutaiCashflowQuarterlyChart = (rows, context) => cashflowChart(rows, { ...context, label: '单季' });
|
||||
|
||||
function revenueMixChart(rows, { width, height, title, subtitle, fields, source }) {
|
||||
const [first, second] = fields;
|
||||
return option({
|
||||
title,
|
||||
subtitle,
|
||||
rows,
|
||||
legend: [first.name, second.name, first.shareName],
|
||||
yAxis: [amountAxis(), percentageAxis(`${first.shareName}(%)`)],
|
||||
series: [
|
||||
{ ...bar(first.name, rows, first.key, first.color), stack: 'revenue' },
|
||||
{ ...bar(second.name, rows, second.key, second.color), stack: 'revenue' },
|
||||
{ ...lineSeries({ name: first.shareName, data: col(rows, first.shareKey).map((value) => (Number.isFinite(value) ? value * 100 : null)), color: PALETTE.red, formatter: (point) => `${point.value.toFixed(1)}%` }), yAxisIndex: 1, z: 10 },
|
||||
],
|
||||
width, height, source,
|
||||
});
|
||||
}
|
||||
|
||||
export const moutaiProductAnnualChart = (rows, context) => revenueMixChart(rows, {
|
||||
...context, title: '贵州茅台产品档次营收(年度)', subtitle: '茅台酒与系列酒营收及茅台酒营收占比',
|
||||
fields: [{ name: '茅台酒', key: 'moutai', shareName: '茅台酒占比', shareKey: 'moutai_share', color: PALETTE.navy }, { name: '系列酒', key: 'series', color: PALETTE.orange }], source: '年度产品档次口径;金额单位为亿元',
|
||||
});
|
||||
export const moutaiChannelAnnualChart = (rows, context) => revenueMixChart(rows, {
|
||||
...context, title: '贵州茅台销售渠道营收(年度)', subtitle: '直销与批发代理营收及直销营收占比',
|
||||
fields: [{ name: '直销', key: 'direct', shareName: '直销占比', shareKey: 'direct_share', color: PALETTE.blue }, { name: '批发代理', key: 'wholesale', color: PALETTE.orange }], source: '年度销售渠道口径;金额单位为亿元',
|
||||
});
|
||||
|
||||
function classifiedMixChart(rows, { width, height, label, kind }) {
|
||||
const isProduct = kind === 'product';
|
||||
return revenueMixChart(rows, {
|
||||
width, height,
|
||||
title: `贵州茅台${isProduct ? '产品档次' : '销售渠道'}营收(${label})`,
|
||||
subtitle: isProduct ? '茅台酒与系列酒营收及茅台酒营收占比' : '直销与批发代理营收及直销营收占比',
|
||||
fields: isProduct
|
||||
? [{ name: '茅台酒', key: 'moutai', shareName: '茅台酒占比', shareKey: 'moutai_share', color: PALETTE.navy }, { name: '系列酒', key: 'series', color: PALETTE.orange }]
|
||||
: [{ name: '直销', key: 'direct', shareName: '直销占比', shareKey: 'direct_share', color: PALETTE.blue }, { name: '批发代理', key: 'wholesale', color: PALETTE.orange }],
|
||||
source: `${label}分类营收口径;金额单位为亿元`,
|
||||
});
|
||||
}
|
||||
|
||||
export const moutaiClassifiedProductQuarterlyChart = (rows, context) => classifiedMixChart(rows, { ...context, label: '单季', kind: 'product' });
|
||||
export const moutaiClassifiedChannelQuarterlyChart = (rows, context) => classifiedMixChart(rows, { ...context, label: '单季', kind: 'channel' });
|
||||
export const moutaiClassifiedProductCumulativeChart = (rows, context) => classifiedMixChart(rows, { ...context, label: '累计', kind: 'product' });
|
||||
export const moutaiClassifiedChannelCumulativeChart = (rows, context) => classifiedMixChart(rows, { ...context, label: '累计', kind: 'channel' });
|
||||
|
||||
export function moutaiRevenueCompositionHalfyearChart(rows, { width, height }) {
|
||||
return option({
|
||||
title: '贵州茅台半年度累计营收构成',
|
||||
subtitle: '实线为产品档次口径,虚线为经营地区口径;两组口径不作加总',
|
||||
rows,
|
||||
legend: ['茅台酒', '系列酒', '国内', '国外'],
|
||||
yAxis: amountAxis(),
|
||||
series: [
|
||||
{ ...lineSeries({ name: '茅台酒', data: col(rows, 'moutai'), color: PALETTE.navy, formatter: (point) => point.value.toFixed(0) }), z: 10 },
|
||||
{ ...lineSeries({ name: '系列酒', data: col(rows, 'series'), color: PALETTE.orange, formatter: (point) => point.value.toFixed(0) }), z: 10 },
|
||||
{ ...lineSeries({ name: '国内', data: col(rows, 'domestic'), color: PALETTE.green, formatter: (point) => point.value.toFixed(0) }), lineStyle: { color: PALETTE.green, width: 2, type: 'dashed' }, z: 9 },
|
||||
{ ...lineSeries({ name: '国外', data: col(rows, 'overseas'), color: PALETTE.purple, formatter: (point) => point.value.toFixed(1) }), lineStyle: { color: PALETTE.purple, width: 2, type: 'dashed' }, z: 9 },
|
||||
],
|
||||
width, height, source: '半年度累计营收构成;金额单位为亿元',
|
||||
});
|
||||
}
|
||||
|
||||
export function moutaiLiquorIndustryOutputChart(rows, { width, height }) {
|
||||
return option({
|
||||
title: '全国白酒产量与同比变化', subtitle: '行业产量趋势,用于观察白酒行业供给环境', rows,
|
||||
legend: ['白酒产量', '同比变化'],
|
||||
yAxis: [amountAxis('白酒产量(万千升)'), percentageAxis('同比变化(%)')],
|
||||
series: [bar('白酒产量', rows, 'output', PALETTE.sage), { ...lineSeries({ name: '同比变化', data: col(rows, 'yoy').map((value) => (Number.isFinite(value) ? value * 100 : null)), color: PALETTE.red, formatter: (point) => `${point.value.toFixed(1)}%` }), yAxisIndex: 1, z: 10 }],
|
||||
width, height, source: '附件“全国白酒产量”工作表',
|
||||
});
|
||||
}
|
||||
|
||||
export function moutaiBaseWineVolumeChart(rows, { width, height }) {
|
||||
return option({
|
||||
title: '茅台基酒产能、产量与商品酒销量', subtitle: '基酒产量与商品酒销售年份存在生产周期差异', rows,
|
||||
legend: ['设计产能', '已释放产能', '基酒产量', '商品酒销量'],
|
||||
yAxis: amountAxis('数量(吨)'),
|
||||
series: [
|
||||
bar('设计产能', rows, 'planned_capacity', PALETTE.sand),
|
||||
bar('基酒产量', rows, 'base_wine_output', PALETTE.navy),
|
||||
{ ...lineSeries({ name: '已释放产能', data: col(rows, 'released_capacity'), color: PALETTE.orange, formatter: (point) => point.value.toFixed(0) }), z: 10 },
|
||||
{ ...lineSeries({ name: '商品酒销量', data: col(rows, 'commercial_wine_sales'), color: PALETTE.green, formatter: (point) => point.value.toFixed(0) }), z: 10 },
|
||||
],
|
||||
width, height, source: '附件“茅台基酒产销量”工作表;单位为吨',
|
||||
});
|
||||
}
|
||||
|
||||
export function moutaiFlyingPriceChart(rows, { width, height }) {
|
||||
return option({
|
||||
title: '飞天茅台年度批价', subtitle: '年度批价为附件记录值,单位为元/瓶', rows,
|
||||
legend: ['飞天茅台批价'], yAxis: amountAxis('批价(元/瓶)'),
|
||||
series: [{ ...lineSeries({ name: '飞天茅台批价', data: col(rows, 'wholesale_price'), color: PALETTE.red, formatter: (point) => point.value.toFixed(0) }), z: 10 }],
|
||||
width, height, source: '附件“飞天茅台历年批价”工作表',
|
||||
});
|
||||
}
|
||||
|
||||
export function moutaiPriceAdjustmentChart(rows, { width, height }) {
|
||||
return option({
|
||||
title: '飞天茅台出厂价调整记录', subtitle: '折线为调整后价格,柱形为本次调价幅度', rows,
|
||||
legend: ['调整后价格', '提价幅度'], yAxis: [amountAxis('出厂价(元/瓶)'), percentageAxis('提价幅度(%)')],
|
||||
series: [
|
||||
{ ...lineSeries({ name: '调整后价格', data: col(rows, 'after_price'), color: PALETTE.navy, formatter: (point) => point.value.toFixed(0) }), z: 10 },
|
||||
{ ...bar('提价幅度', rows, 'increase_rate', PALETTE.orange), yAxisIndex: 1, data: col(rows, 'increase_rate').map((value) => (Number.isFinite(value) ? value * 100 : null)) },
|
||||
], width, height, source: '附件“提价记录”工作表',
|
||||
});
|
||||
}
|
||||
|
||||
export function moutaiTonPriceChart(rows, { width, height }) {
|
||||
return option({
|
||||
title: '茅台酒与系列酒平均吨价', subtitle: '由各产品收入除以商品酒销量计算,单位为万元/吨', rows,
|
||||
legend: ['茅台酒平均吨价', '系列酒平均吨价'], yAxis: amountAxis('万元/吨'),
|
||||
series: [
|
||||
{ ...lineSeries({ name: '茅台酒平均吨价', data: col(rows, 'moutai_price_per_ton'), color: PALETTE.navy, formatter: (point) => point.value.toFixed(1) }), z: 10 },
|
||||
{ ...lineSeries({ name: '系列酒平均吨价', data: col(rows, 'series_price_per_ton'), color: PALETTE.orange, formatter: (point) => point.value.toFixed(1) }), z: 10 },
|
||||
], width, height, source: '附件“吨价”工作表;由收入与销量计算',
|
||||
});
|
||||
}
|
||||
|
||||
export function moutaiDividendChart(rows, { width, height }) {
|
||||
return option({
|
||||
title: '贵州茅台分红与归母净利润', subtitle: '分红总金额与归母净利润;折线为分红率', rows,
|
||||
legend: ['分红总金额', '归母净利润', '分红率'], yAxis: [amountAxis(), percentageAxis('分红率(%)')],
|
||||
series: [
|
||||
bar('分红总金额', rows, 'dividend', PALETTE.orange),
|
||||
bar('归母净利润', rows, 'net_profit', PALETTE.navy),
|
||||
{ ...lineSeries({ name: '分红率', data: col(rows, 'payout_ratio').map((value) => (Number.isFinite(value) ? value * 100 : null)), color: PALETTE.red, formatter: (point) => `${point.value.toFixed(1)}%` }), yAxisIndex: 1, z: 10 },
|
||||
], width, height, source: '附件“历年分红”工作表;金额单位为亿元',
|
||||
});
|
||||
}
|
||||
|
||||
export function moutaiDistributorsChart(rows, { width, height }) {
|
||||
return option({
|
||||
title: '贵州茅台经销商数量', subtitle: '国内与国外经销商期末数量', rows,
|
||||
legend: ['国内经销商', '国外经销商', '合计'], yAxis: amountAxis('数量(家)'),
|
||||
series: [
|
||||
{ ...bar('国内经销商', rows, 'domestic', PALETTE.navy), stack: 'dealer' },
|
||||
{ ...bar('国外经销商', rows, 'overseas', PALETTE.orange), stack: 'dealer' },
|
||||
{ ...lineSeries({ name: '合计', data: col(rows, 'total'), color: PALETTE.red, formatter: (point) => point.value.toFixed(0) }), z: 10 },
|
||||
], width, height, source: '附件“经销商数量”工作表;单位为家',
|
||||
});
|
||||
}
|
||||
+56
-37
@@ -1,16 +1,45 @@
|
||||
import { col } from '../csv.js';
|
||||
import {
|
||||
PALETTE, FONT, axisCommon, standalonePeriodAxis, titleStyle, legendStyle,
|
||||
financialViewLabel,
|
||||
financialViewLabel, niceAxisMax, fanTinyLabelOffsets, stackedSharePoint,
|
||||
lineSeries, footnote, watermarks, LAYOUT,
|
||||
} from '../theme.js';
|
||||
|
||||
const KEYS = ['cash_like', 'receivables', 'investment', 'operating_assets'];
|
||||
const NAMES = {
|
||||
cash_like: '类现金',
|
||||
receivables: '应收账款',
|
||||
investment: '投资资产',
|
||||
operating_assets: '经营类资产',
|
||||
};
|
||||
const COLORS = {
|
||||
cash_like: PALETTE.blue,
|
||||
receivables: PALETTE.orange,
|
||||
investment: PALETTE.purple,
|
||||
operating_assets: PALETTE.sage,
|
||||
};
|
||||
// 段内标签文字色:浅色段(经营类资产灰绿)用深字,深色段用白字
|
||||
const INSIDE_LABEL_COLORS = {
|
||||
cash_like: '#fff',
|
||||
receivables: '#fff',
|
||||
investment: '#fff',
|
||||
operating_assets: PALETTE.ink,
|
||||
};
|
||||
|
||||
/**
|
||||
* 泡泡玛特资产结构演变图
|
||||
* 堆叠柱状图展示资产配置,折线图展示总资产增长
|
||||
* 堆叠柱状图展示资产配置(每段标数值),折线图展示总资产增长
|
||||
*/
|
||||
export function assetsChart(rows, { width, height, view = 'all' }) {
|
||||
const periods = col(rows, 'period');
|
||||
const totalMax = Math.max(...rows.map((r) => r.total_assets));
|
||||
// 轴上限随数据走(2025H2 总资产已 321 亿),矮段像素判定才有确定基准
|
||||
const axisMax = niceAxisMax(totalMax);
|
||||
const interval = axisMax % 7 === 0 ? axisMax / 7 : axisMax / 5;
|
||||
// 绘图区像素高:画布高 − grid top 145 − grid bottom 90
|
||||
const pxPerUnit = (height - LAYOUT.gridTop - 90) / axisMax;
|
||||
// 早期年份应收/投资不足 1 亿,矮段标签外置到柱体右侧扇形排开
|
||||
const rowOffsets = fanTinyLabelOffsets(rows, KEYS, pxPerUnit);
|
||||
|
||||
return {
|
||||
backgroundColor: '#fff',
|
||||
@@ -22,7 +51,7 @@ export function assetsChart(rows, { width, height, view = 'all' }) {
|
||||
legend: {
|
||||
...legendStyle,
|
||||
top: LAYOUT.legendTop,
|
||||
data: ['类现金', '应收账款', '投资资产', '经营类资产', '资产总计'],
|
||||
data: [...KEYS.map((key) => NAMES[key]), '资产总计'],
|
||||
},
|
||||
grid: { left: 78, right: 82, top: LAYOUT.gridTop, bottom: 90 },
|
||||
tooltip: {
|
||||
@@ -40,8 +69,8 @@ export function assetsChart(rows, { width, height, view = 'all' }) {
|
||||
nameGap: 52,
|
||||
nameTextStyle: { color: PALETTE.grey, fontSize: 13, fontFamily: FONT },
|
||||
min: 0,
|
||||
max: 350,
|
||||
interval: 50,
|
||||
max: axisMax,
|
||||
interval,
|
||||
},
|
||||
{
|
||||
...axisCommon,
|
||||
@@ -52,44 +81,34 @@ export function assetsChart(rows, { width, height, view = 'all' }) {
|
||||
nameRotate: -90,
|
||||
nameTextStyle: { color: PALETTE.grey, fontSize: 13, fontFamily: FONT },
|
||||
min: 0,
|
||||
max: 350,
|
||||
interval: 50,
|
||||
max: axisMax,
|
||||
interval,
|
||||
splitLine: { show: false },
|
||||
},
|
||||
],
|
||||
series: [
|
||||
{
|
||||
name: '类现金',
|
||||
...KEYS.map((key, ci) => ({
|
||||
name: NAMES[key],
|
||||
type: 'bar',
|
||||
stack: 'assets',
|
||||
data: col(rows, 'cash_like'),
|
||||
itemStyle: { color: PALETTE.blue },
|
||||
// 每段都标数值:矮段塞不进字的,外置到柱体右侧(机制同渠道金额图)
|
||||
data: rows.map((row, ri) => stackedSharePoint(row[key], {
|
||||
pxPerUnit,
|
||||
labelColor: INSIDE_LABEL_COLORS[key],
|
||||
dy: rowOffsets[ri][ci] ?? 0,
|
||||
})),
|
||||
itemStyle: { color: COLORS[key] },
|
||||
emphasis: { focus: 'series' },
|
||||
},
|
||||
{
|
||||
name: '应收账款',
|
||||
type: 'bar',
|
||||
stack: 'assets',
|
||||
data: col(rows, 'receivables'),
|
||||
itemStyle: { color: PALETTE.orange },
|
||||
emphasis: { focus: 'series' },
|
||||
},
|
||||
{
|
||||
name: '投资资产',
|
||||
type: 'bar',
|
||||
stack: 'assets',
|
||||
data: col(rows, 'investment'),
|
||||
itemStyle: { color: PALETTE.purple },
|
||||
emphasis: { focus: 'series' },
|
||||
},
|
||||
{
|
||||
name: '经营类资产',
|
||||
type: 'bar',
|
||||
stack: 'assets',
|
||||
data: col(rows, 'operating_assets'),
|
||||
itemStyle: { color: PALETTE.sage },
|
||||
emphasis: { focus: 'series' },
|
||||
},
|
||||
label: {
|
||||
show: true,
|
||||
position: 'inside',
|
||||
color: INSIDE_LABEL_COLORS[key],
|
||||
fontSize: 11,
|
||||
fontFamily: FONT,
|
||||
overflow: 'none',
|
||||
formatter: (p) => `${p.value.toFixed(1)}`,
|
||||
},
|
||||
})),
|
||||
{
|
||||
...lineSeries({
|
||||
name: '资产总计',
|
||||
@@ -105,7 +124,7 @@ export function assetsChart(rows, { width, height, view = 'all' }) {
|
||||
],
|
||||
graphic: [
|
||||
...watermarks(width, height),
|
||||
footnote('数据来源:泡泡玛特财报 | @思考的Joey'),
|
||||
footnote('数据来源:泡泡玛特财报 | @Joey知行记'),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -109,7 +109,7 @@ export function ppmtBridgeChart(rows, { width, height }) {
|
||||
...watermarks(width, height),
|
||||
footnote(
|
||||
'各项为占营收百分点,已折算为对净利率影响(×税盾系数)\n' +
|
||||
'其他收益净额:26H1净损失6.89亿,主要为汇兑损失7.20亿(25H1为汇兑收益1.20亿) | @思考的Joey'
|
||||
'其他收益净额:26H1净损失6.89亿,主要为汇兑损失7.20亿(25H1为汇兑收益1.20亿) | @Joey知行记'
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
@@ -89,7 +89,7 @@ export function cashConversionChart(rows, { width, height, view = 'all' }) {
|
||||
],
|
||||
graphic: [
|
||||
...watermarks(width, height),
|
||||
footnote('数据来源:泡泡玛特合并现金流量表、利润表;H2为单独半年 | @思考的Joey'),
|
||||
footnote('数据来源:泡泡玛特合并现金流量表、利润表;H2为单独半年 | @Joey知行记'),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@ export function cashflowCompositionChart(rows, { width, height, view = 'all' })
|
||||
],
|
||||
graphic: [
|
||||
...watermarks(width, height),
|
||||
footnote('数据来源:泡泡玛特合并现金流量表;H2为单独半年 | @思考的Joey'),
|
||||
footnote('数据来源:泡泡玛特合并现金流量表;H2为单独半年 | @Joey知行记'),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
+130
-77
@@ -1,66 +1,150 @@
|
||||
import { col } from '../csv.js';
|
||||
import {
|
||||
PALETTE, FONT, axisCommon, standalonePeriodAxis, titleStyle, legendStyle,
|
||||
financialViewDisclosure, financialViewLabel,
|
||||
footnote, watermarks, LAYOUT,
|
||||
financialViewDisclosure, financialViewLabel, niceAxisMax,
|
||||
fanTinyLabelOffsets, stackedSharePoint,
|
||||
lineSeries, footnote, watermarks, LAYOUT,
|
||||
} from '../theme.js';
|
||||
|
||||
/**
|
||||
* 泡泡玛特品类收入结构图
|
||||
* 堆叠柱状图展示各品类的收入占比和增长
|
||||
* 堆叠柱展示各品类收入;折线(右轴)展示各品类收入同比,折线与柱同色便于对应。
|
||||
* 同比为衍生指标:当期 ÷ 上年同期 − 1(H2 对 H2 单半年、FY 对 FY 累计,同期对同期),
|
||||
* 2024 年各期无上年基数,不计算。
|
||||
*/
|
||||
// 同比折线用柱色的同色系深色变体:压在柱体上能区分开,又靠色相保持与柱的对应
|
||||
const CATEGORIES = [
|
||||
{ name: '毛绒', key: 'plush', color: PALETTE.coral, lineColor: PALETTE.red },
|
||||
{ name: '手办', key: 'figure', color: PALETTE.blue, lineColor: PALETTE.navy },
|
||||
// 色板没有深紫,按 MEGA 紫加深
|
||||
{ name: 'MEGA', key: 'mega', color: PALETTE.purple, lineColor: '#5E2A79' },
|
||||
{ name: '衍生品及其他', key: 'other', color: PALETTE.sage, lineColor: PALETTE.green },
|
||||
];
|
||||
|
||||
export function categoryChart(rows, { width, height, view = 'all' }) {
|
||||
// 过滤掉表头行("品类"那一行)
|
||||
const dataRows = rows.filter(r => r.period && r.period !== '品类');
|
||||
const periods = col(dataRows, 'period');
|
||||
const num = (r, k) => (Number.isFinite(r[k]) ? r[k] : null);
|
||||
|
||||
// 上年同期的期间下标(H1↔H1、H2↔H2、FY↔FY),无则 -1
|
||||
const priorIndex = periods.map((p) => {
|
||||
if (!/^20\d{2}(H1|H2|FY)$/.test(p)) return -1;
|
||||
const prev = p.replace(/^20(\d{2})/, (_, yy) => `20${String(Number(yy) - 1).padStart(2, '0')}`);
|
||||
return periods.indexOf(prev);
|
||||
});
|
||||
const yoyByCat = Object.fromEntries(CATEGORIES.map((c) => [
|
||||
c.key,
|
||||
periods.map((_, i) => {
|
||||
const j = priorIndex[i];
|
||||
const cur = num(dataRows[i], c.key);
|
||||
const prev = j >= 0 ? num(dataRows[j], c.key) : null;
|
||||
if (cur == null || prev == null || prev === 0) return null;
|
||||
return (cur / prev - 1) * 100;
|
||||
}),
|
||||
]));
|
||||
const yoyValues = CATEGORIES.flatMap((c) => yoyByCat[c.key]).filter(Number.isFinite);
|
||||
const yoyMax = Math.max(100, Math.ceil(Math.max(...yoyValues) / 100) * 100);
|
||||
const yoyMin = Math.min(0, Math.floor(Math.min(...yoyValues) / 100) * 100);
|
||||
// 右轴固定整百刻度:范围 ≤500 时 100% 一步,否则 200% 一步,刻度数控制在 7 以内
|
||||
const yoyInterval = yoyMax - yoyMin <= 500 ? 100 : 200;
|
||||
|
||||
// 多折线标签避让(同 ppmt_margins labeledData):比较上下间隙,朝更大的一侧放
|
||||
const yoyLabelPos = periods.map((_, i) => {
|
||||
const ranked = CATEGORIES
|
||||
.map((c) => yoyByCat[c.key][i])
|
||||
.filter(Number.isFinite)
|
||||
.sort((a, b) => b - a);
|
||||
return Object.fromEntries(CATEGORIES.map((c) => {
|
||||
const v = yoyByCat[c.key][i];
|
||||
if (!Number.isFinite(v)) return [c.key, 'top'];
|
||||
const rank = ranked.indexOf(v);
|
||||
const gapUp = rank > 0 ? ranked[rank - 1] - v : Infinity;
|
||||
const gapDown = rank < ranked.length - 1 ? v - ranked[rank + 1] : Infinity;
|
||||
return [c.key, gapUp >= gapDown ? 'top' : 'bottom'];
|
||||
}));
|
||||
});
|
||||
|
||||
const totals = periods.map((_, i) => CATEGORIES.reduce((s, c) => s + (num(dataRows[i], c.key) ?? 0), 0));
|
||||
// 轴上限随数据走:2025FY 合计已 371.2 亿,写死 250 会把柱子顶出绘图区
|
||||
const axisMax = niceAxisMax(Math.max(...totals));
|
||||
const amountInterval = axisMax % 4 === 0 ? axisMax / 4 : axisMax / 5;
|
||||
// 绘图区像素高:画布高 − grid top 145 − grid bottom 90
|
||||
const pxPerUnit = (height - LAYOUT.gridTop - 90) / axisMax;
|
||||
const rowOffsets = fanTinyLabelOffsets(dataRows, CATEGORIES.map((c) => c.key), pxPerUnit);
|
||||
|
||||
return {
|
||||
backgroundColor: '#fff',
|
||||
title: {
|
||||
...titleStyle,
|
||||
text: `泡泡玛特品类收入结构(${financialViewLabel(view)})`,
|
||||
subtext: ['单位:亿元;H2为下半年单期', financialViewDisclosure(view)].filter(Boolean).join('\n'),
|
||||
subtext: ['柱为收入(亿元,左轴);折线为各品类收入同比(右轴);H2为下半年单期',
|
||||
financialViewDisclosure(view)].filter(Boolean).join('\n'),
|
||||
},
|
||||
// 图例柱与同比线交错排列,同品类的两项相邻
|
||||
legend: {
|
||||
...legendStyle,
|
||||
top: LAYOUT.legendTop,
|
||||
data: ['毛绒', '手办', 'MEGA', '衍生品及其他'],
|
||||
data: CATEGORIES.flatMap((c) => [c.name, `${c.name}同比`]),
|
||||
},
|
||||
grid: { left: 78, right: 56, top: LAYOUT.gridTop, bottom: 90 },
|
||||
grid: { left: 78, right: 82, top: LAYOUT.gridTop, bottom: 90 },
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: { type: 'shadow' },
|
||||
textStyle: { fontFamily: FONT },
|
||||
formatter: (params) => {
|
||||
const bars = params.filter((p) => p.seriesType === 'bar' && Number.isFinite(p.value));
|
||||
const lines = params.filter((p) => p.seriesType === 'line' && Number.isFinite(p.value));
|
||||
const total = bars.reduce((s, p) => s + p.value, 0);
|
||||
let result = `<b>${params[0].axisValue}</b><br/>`;
|
||||
let total = 0;
|
||||
params.forEach(p => {
|
||||
total += p.value;
|
||||
result += `${p.marker}${p.seriesName}: ${p.value.toFixed(2)} 亿元<br/>`;
|
||||
bars.forEach((p) => { result += `${p.marker}${p.seriesName}: ${p.value.toFixed(2)} 亿元<br/>`; });
|
||||
result += `<b>合计: ${total.toFixed(2)} 亿元</b><br/>`;
|
||||
lines.forEach((p) => {
|
||||
result += `${p.marker}${p.seriesName}: ${p.value > 0 ? '+' : ''}${p.value.toFixed(1)}%<br/>`;
|
||||
});
|
||||
result += `<b>合计: ${total.toFixed(2)} 亿元</b>`;
|
||||
return result;
|
||||
},
|
||||
},
|
||||
xAxis: { ...standalonePeriodAxis, data: periods },
|
||||
yAxis: {
|
||||
...axisCommon,
|
||||
type: 'value',
|
||||
name: '收入(亿元)',
|
||||
nameLocation: 'middle',
|
||||
nameGap: 52,
|
||||
nameTextStyle: { color: PALETTE.grey, fontSize: 13, fontFamily: FONT },
|
||||
min: 0,
|
||||
max: 250,
|
||||
interval: 50,
|
||||
},
|
||||
yAxis: [
|
||||
{
|
||||
...axisCommon,
|
||||
type: 'value',
|
||||
name: '收入(亿元)',
|
||||
nameLocation: 'middle',
|
||||
nameGap: 52,
|
||||
nameTextStyle: { color: PALETTE.grey, fontSize: 13, fontFamily: FONT },
|
||||
min: 0,
|
||||
max: axisMax,
|
||||
interval: amountInterval,
|
||||
},
|
||||
{
|
||||
...axisCommon,
|
||||
type: 'value',
|
||||
name: '收入同比(%)',
|
||||
nameLocation: 'middle',
|
||||
nameGap: 56,
|
||||
nameRotate: -90,
|
||||
nameTextStyle: { color: PALETTE.grey, fontSize: 13, fontFamily: FONT },
|
||||
min: yoyMin,
|
||||
max: yoyMax,
|
||||
interval: yoyInterval,
|
||||
axisLabel: { ...axisCommon.axisLabel, formatter: '{value}%' },
|
||||
splitLine: { show: false },
|
||||
},
|
||||
],
|
||||
series: [
|
||||
{
|
||||
name: '毛绒',
|
||||
...CATEGORIES.map((c, ci) => ({
|
||||
name: c.name,
|
||||
type: 'bar',
|
||||
stack: 'category',
|
||||
data: col(dataRows, 'plush'),
|
||||
itemStyle: { color: PALETTE.coral },
|
||||
// 每段都标数值:矮段外置到柱体右侧,机制同渠道金额图
|
||||
data: dataRows.map((row, ri) => stackedSharePoint(row[c.key], {
|
||||
pxPerUnit,
|
||||
labelColor: '#fff',
|
||||
dy: rowOffsets[ri][ci] ?? 0,
|
||||
})),
|
||||
itemStyle: { color: c.color },
|
||||
emphasis: { focus: 'series' },
|
||||
label: {
|
||||
show: true,
|
||||
@@ -68,61 +152,30 @@ export function categoryChart(rows, { width, height, view = 'all' }) {
|
||||
color: '#fff',
|
||||
fontSize: 12,
|
||||
fontFamily: FONT,
|
||||
formatter: (p) => (p.value > 15 ? p.value.toFixed(1) : ''),
|
||||
overflow: 'none',
|
||||
formatter: (p) => `${p.value.toFixed(1)}`,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: '手办',
|
||||
type: 'bar',
|
||||
stack: 'category',
|
||||
data: col(dataRows, 'figure'),
|
||||
itemStyle: { color: PALETTE.blue },
|
||||
emphasis: { focus: 'series' },
|
||||
label: {
|
||||
show: true,
|
||||
position: 'inside',
|
||||
color: '#fff',
|
||||
fontSize: 12,
|
||||
fontFamily: FONT,
|
||||
formatter: (p) => (p.value > 15 ? p.value.toFixed(1) : ''),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'MEGA',
|
||||
type: 'bar',
|
||||
stack: 'category',
|
||||
data: col(dataRows, 'mega'),
|
||||
itemStyle: { color: PALETTE.purple },
|
||||
emphasis: { focus: 'series' },
|
||||
label: {
|
||||
show: true,
|
||||
position: 'inside',
|
||||
color: '#fff',
|
||||
fontSize: 12,
|
||||
fontFamily: FONT,
|
||||
formatter: (p) => (p.value > 8 ? p.value.toFixed(1) : ''),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: '衍生品及其他',
|
||||
type: 'bar',
|
||||
stack: 'category',
|
||||
data: col(dataRows, 'other'),
|
||||
itemStyle: { color: PALETTE.sage },
|
||||
emphasis: { focus: 'series' },
|
||||
label: {
|
||||
show: true,
|
||||
position: 'inside',
|
||||
color: '#fff',
|
||||
fontSize: 12,
|
||||
fontFamily: FONT,
|
||||
formatter: (p) => (p.value > 8 ? p.value.toFixed(1) : ''),
|
||||
},
|
||||
},
|
||||
})),
|
||||
...CATEGORIES.map((c) => ({
|
||||
...lineSeries({
|
||||
name: `${c.name}同比`,
|
||||
data: yoyByCat[c.key],
|
||||
color: c.lineColor,
|
||||
formatter: (p) => `${p.value.toFixed(1)}%`,
|
||||
width: 2.5,
|
||||
}),
|
||||
yAxisIndex: 1,
|
||||
z: 10,
|
||||
// label.position 不接受函数,逐点覆盖(避让规则见 yoyLabelPos)
|
||||
data: yoyByCat[c.key].map((v, i) => (Number.isFinite(v)
|
||||
? { value: v, label: { position: yoyLabelPos[i][c.key], distance: 10 } }
|
||||
: null)),
|
||||
labelLayout: { hideOverlap: true },
|
||||
})),
|
||||
],
|
||||
graphic: [
|
||||
...watermarks(width, height),
|
||||
footnote('数据来源:泡泡玛特财报 | @思考的Joey'),
|
||||
footnote('数据来源:泡泡玛特财报;同比=当期÷上年同期−1\n2024年各期无上年基数,不计算同比 | @Joey知行记'),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { col } from '../csv.js';
|
||||
import {
|
||||
CHANNEL_COLORS, PALETTE, FONT, axisCommon, standalonePeriodAxis, titleStyle, legendStyle,
|
||||
financialViewDisclosure, financialViewLabel,
|
||||
financialViewDisclosure, financialViewLabel, niceAxisMax,
|
||||
lineSeries, footnote, watermarks, LAYOUT,
|
||||
} from '../theme.js';
|
||||
|
||||
@@ -23,14 +23,12 @@ const INSIDE_LABEL_COLORS = {
|
||||
|
||||
const num = (value) => (Number.isFinite(value) ? value : null);
|
||||
|
||||
// 轴上限取整齐刻度,柱高比例确定后,段标签的段内/外置可按像素判断
|
||||
const NICE_MAX = [20, 25, 50, 60, 80, 100, 120, 150, 200, 250, 300, 400, 500, 600, 800, 1000];
|
||||
// 轴上限按数据最大值取整齐刻度,柱高比例确定后,段标签的段内/外置可按像素判断
|
||||
const MIN_INSIDE_PX = 13; // 低于约 13px 的段放不下 10px 段内标签
|
||||
|
||||
export function channelRevenueChart(rows, { width, height, view = 'all' }) {
|
||||
const totalMax = Math.max(...rows.map((row) => num(row.total_revenue) || 0));
|
||||
const axisMax = NICE_MAX.find((max) => max >= totalMax * 1.02)
|
||||
?? Math.ceil((totalMax * 1.2) / 100) * 100;
|
||||
const axisMax = niceAxisMax(totalMax);
|
||||
const yoyValues = col(rows, 'revenue_yoy').filter(Number.isFinite);
|
||||
// 全部/半年视图存在 2022H2 的负增长,右轴需包含负区间,避免折线被 0 轴裁切。
|
||||
const yoyMin = Math.min(0, Math.floor(Math.min(...yoyValues) / 50) * 50);
|
||||
@@ -170,7 +168,7 @@ export function channelRevenueChart(rows, { width, height, view = 'all' }) {
|
||||
],
|
||||
graphic: [
|
||||
...watermarks(width, height),
|
||||
footnote('数据来源:泡泡玛特营收构成表;渠道口径按财报披露 | @思考的Joey'),
|
||||
footnote('数据来源:泡泡玛特营收构成表;渠道口径按财报披露 | @Joey知行记'),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
@@ -77,7 +74,7 @@ export function channelShareChart(rows, { width, height, view = 'all' }) {
|
||||
})),
|
||||
graphic: [
|
||||
...watermarks(width, height),
|
||||
footnote('数据来源:泡泡玛特分地区渠道收入披露(中国含港澳台);H2为单半年(全年−H1)\n线上渠道含抽盒机、天猫、抖音、自研APP及官网等 | @思考的Joey'),
|
||||
footnote('数据来源:泡泡玛特分地区渠道收入披露(中国含港澳台);H2为单半年(全年−H1)\n线上渠道含抽盒机、天猫、抖音、自研APP及官网等 | @Joey知行记'),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,205 @@
|
||||
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知行记',
|
||||
},
|
||||
};
|
||||
|
||||
/** 轴上限取整齐刻度,柱高比例确定后,段标签的段内/外置可按像素判断(同 ppmt_channel_revenue)。 */
|
||||
const AMOUNT_NICE_MAX = [10, 20, 25, 50, 60, 80, 100, 120, 150, 200, 250, 300, 400, 500];
|
||||
|
||||
/**
|
||||
* 分地区渠道收入金额堆叠柱:与占比图同源同色,直接使用披露金额(亿元),
|
||||
* 不做占比×总收入还原。矮段标签外置到柱体右侧的机制同全局渠道金额图。
|
||||
*/
|
||||
export function makeRegionChannelAmountChart(regionKey) {
|
||||
const region = REGIONS[regionKey];
|
||||
if (!region) throw new Error(`未配置的地区渠道金额图:${regionKey}`);
|
||||
return function regionChannelAmountChart(rows, { width, height, view = 'all' }) {
|
||||
const periods = col(rows, 'period');
|
||||
const barWidth = view === 'all' ? 48 : 80;
|
||||
const totalMax = Math.max(...rows.map((row) => row.total_revenue));
|
||||
const axisMax = AMOUNT_NICE_MAX.find((max) => max >= totalMax * 1.02)
|
||||
?? Math.ceil((totalMax * 1.2) / 100) * 100;
|
||||
// 绘图区像素高:画布高 − grid top 145 − grid bottom 100(两行脚注)
|
||||
const pxPerUnit = (height - LAYOUT.gridTop - 100) / axisMax;
|
||||
const rowOffsets = fanTinyLabelOffsets(rows, region.series, pxPerUnit);
|
||||
|
||||
return {
|
||||
backgroundColor: '#fff',
|
||||
title: {
|
||||
...titleStyle,
|
||||
text: `泡泡玛特${region.title}渠道收入金额(${financialViewLabel(view)})`,
|
||||
subtext: '堆叠柱为各渠道收入(亿元);矮段数值标注在柱体右侧,线上渠道按披露细分拆分',
|
||||
},
|
||||
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: 82, top: LAYOUT.gridTop, bottom: 100 },
|
||||
xAxis: { ...standalonePeriodAxis, data: periods },
|
||||
yAxis: {
|
||||
...axisCommon,
|
||||
type: 'value',
|
||||
name: '渠道收入(亿元)',
|
||||
nameLocation: 'middle',
|
||||
nameGap: 56,
|
||||
nameTextStyle: { color: PALETTE.grey, fontSize: 13, fontFamily: FONT },
|
||||
min: 0,
|
||||
max: axisMax,
|
||||
interval: axisMax % 4 === 0 ? axisMax / 4 : axisMax / 5,
|
||||
},
|
||||
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-amount-${regionKey}`,
|
||||
label: {
|
||||
show: true,
|
||||
position: 'inside',
|
||||
fontSize: 11,
|
||||
fontFamily: FONT,
|
||||
fontWeight: 500,
|
||||
overflow: 'none',
|
||||
formatter: (point) => point.value.toFixed(1),
|
||||
},
|
||||
emphasis: { focus: 'series' },
|
||||
})),
|
||||
graphic: [
|
||||
...watermarks(width, height),
|
||||
footnote(region.footnote),
|
||||
],
|
||||
};
|
||||
};
|
||||
}
|
||||
/** 分地区渠道收入占比:线上渠道拆分披露的细分平台,每根柱按当期该地区总收入归一化。 */
|
||||
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),
|
||||
],
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -59,7 +59,7 @@ export function ppmtCoreMarginChart(rows, { width, height, view = 'all' }) {
|
||||
...watermarks(width, height),
|
||||
footnote(
|
||||
'核心利润率由绝对值口径直接计算(核心利润÷营收),比三个比率行相减更准\n' +
|
||||
'——后者各只存4位小数,累加误差可达0.045pct | @思考的Joey'
|
||||
'——后者各只存4位小数,累加误差可达0.045pct | @Joey知行记'
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
@@ -137,7 +137,7 @@ export function ppmtCoreProfitChart(rows, { width, height, view = 'all' }) {
|
||||
...watermarks(width, height),
|
||||
footnote(
|
||||
'核心利润反映主业盈利能力(扣除营销与管理成本);' +
|
||||
'2025H1/H2同比增速受益于费用率优化和规模效应 | @思考的Joey'
|
||||
'2025H1/H2同比增速受益于费用率优化和规模效应 | @Joey知行记'
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
@@ -1,12 +1,49 @@
|
||||
import { col } from '../csv.js';
|
||||
import {
|
||||
PALETTE, FONT, axisCommon, standalonePeriodAxis, titleStyle, legendStyle,
|
||||
financialViewDisclosure, financialViewLabel,
|
||||
financialViewDisclosure, financialViewLabel, niceAxisMax, stackedSharePoint,
|
||||
lineSeries, footnote, watermarks, LAYOUT,
|
||||
} from '../theme.js';
|
||||
|
||||
/**
|
||||
* 混合正负堆叠的矮段标签外置偏移。
|
||||
* ECharts 对负值段自 0 向下堆叠,theme 的 fanTinyLabelOffsets 按全正累计
|
||||
* 算不出正确中心,这里按符号分两摞累计(屏幕坐标 y 向下为正),
|
||||
* 矮段(塞不进段内文字的)标签扇形排开在柱体右侧。
|
||||
*/
|
||||
function expenseFanOffsets(rows, keys, pxPerUnit, { minInsidePx = 15, gap = 13 } = {}) {
|
||||
return rows.map((row) => {
|
||||
let posCum = 0;
|
||||
let negCum = 0;
|
||||
const centers = {};
|
||||
keys.forEach((k) => {
|
||||
const v = Number.isFinite(row[k]) ? row[k] : 0;
|
||||
const mid = v >= 0 ? posCum + v / 2 : negCum + v / 2;
|
||||
centers[k] = -mid * pxPerUnit;
|
||||
if (v >= 0) posCum += v; else negCum += v;
|
||||
});
|
||||
const tiny = keys.filter((k) => Number.isFinite(row[k]) && row[k] !== 0
|
||||
&& Math.abs(row[k]) * pxPerUnit < minInsidePx);
|
||||
if (!tiny.length) return {};
|
||||
const ys = tiny.map((k) => centers[k]);
|
||||
for (let i = 1; i < ys.length; i += 1) ys[i] = Math.max(ys[i], ys[i - 1] + gap);
|
||||
const offsets = {};
|
||||
tiny.forEach((k, i) => { offsets[k] = centers[k] - ys[i]; });
|
||||
return offsets;
|
||||
});
|
||||
}
|
||||
|
||||
/** 泡泡玛特期间费用结构:观察销售投入、管理成本与费用率的规模效应。 */
|
||||
export function expenseStructureChart(rows, { width, height, view = 'all' }) {
|
||||
const keys = ['sales_expense', 'admin_expense', 'financial_expense'];
|
||||
const posStackMax = Math.max(...rows.map((r) => keys.reduce((s, k) => s + Math.max(r[k] ?? 0, 0), 0)));
|
||||
const negMin = Math.min(...rows.map((r) => keys.reduce((s, k) => s + Math.min(r[k] ?? 0, 0), 0)));
|
||||
// 轴范围随数据走并取整刻度,保证矮段像素判定(pxPerUnit)有确定基准
|
||||
const axisMax = niceAxisMax(posStackMax);
|
||||
const axisMin = Math.min(0, Math.floor(negMin / 10) * 10);
|
||||
const pxPerUnit = (height - LAYOUT.gridTop - 90) / (axisMax - axisMin);
|
||||
const rowOffsets = expenseFanOffsets(rows, keys, pxPerUnit);
|
||||
|
||||
return {
|
||||
backgroundColor: '#fff',
|
||||
title: {
|
||||
@@ -35,6 +72,9 @@ export function expenseStructureChart(rows, { width, height, view = 'all' }) {
|
||||
nameLocation: 'middle',
|
||||
nameGap: 52,
|
||||
nameTextStyle: { color: PALETTE.grey, fontSize: 13, fontFamily: FONT },
|
||||
min: axisMin,
|
||||
max: axisMax,
|
||||
interval: 10,
|
||||
},
|
||||
{
|
||||
...axisCommon,
|
||||
@@ -52,9 +92,34 @@ export function expenseStructureChart(rows, { width, height, view = 'all' }) {
|
||||
},
|
||||
],
|
||||
series: [
|
||||
{ name: '销售费用', type: 'bar', stack: 'expenses', data: col(rows, 'sales_expense'), itemStyle: { color: PALETTE.orange } },
|
||||
{ name: '管理费用', type: 'bar', stack: 'expenses', data: col(rows, 'admin_expense'), itemStyle: { color: PALETTE.purple } },
|
||||
{ name: '财务费用(净额)', type: 'bar', stack: 'expenses', data: col(rows, 'financial_expense'), itemStyle: { color: PALETTE.blue } },
|
||||
...keys.map((key, ci) => ({
|
||||
name: { sales_expense: '销售费用', admin_expense: '管理费用', financial_expense: '财务费用(净额)' }[key],
|
||||
type: 'bar',
|
||||
stack: 'expenses',
|
||||
// 每段都标数值:矮段(含负值段)外置到柱体右侧,见 expenseFanOffsets
|
||||
data: rows.map((row, ri) => stackedSharePoint(row[key], {
|
||||
pxPerUnit,
|
||||
labelColor: '#fff',
|
||||
dy: rowOffsets[ri][key] ?? 0,
|
||||
})),
|
||||
itemStyle: { color: [PALETTE.orange, PALETTE.purple, PALETTE.blue][ci] },
|
||||
emphasis: { focus: 'series' },
|
||||
label: {
|
||||
show: true,
|
||||
position: 'inside',
|
||||
color: '#fff',
|
||||
fontSize: 11,
|
||||
fontFamily: FONT,
|
||||
fontWeight: 500,
|
||||
overflow: 'none',
|
||||
// 财务费用净额多在 ±0.1 亿内,一位小数会显示成 ±0.0,此时退到两位小数
|
||||
formatter: (p) => {
|
||||
const t = p.value.toFixed(1);
|
||||
if (t === '0.0' || t === '-0.0') return Math.abs(p.value) < 0.005 ? '0.0' : p.value.toFixed(2);
|
||||
return t;
|
||||
},
|
||||
},
|
||||
})),
|
||||
{
|
||||
...lineSeries({
|
||||
name: '四项费用率',
|
||||
@@ -69,7 +134,7 @@ export function expenseStructureChart(rows, { width, height, view = 'all' }) {
|
||||
],
|
||||
graphic: [
|
||||
...watermarks(width, height),
|
||||
footnote('数据来源:泡泡玛特合并利润表;H2为单独半年 | @思考的Joey'),
|
||||
footnote('数据来源:泡泡玛特合并利润表;H2为单独半年 | @Joey知行记'),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -106,7 +106,7 @@ export function ppmtGrowthChart(rows, { width, height, view = 'all' }) {
|
||||
})),
|
||||
graphic: [
|
||||
...watermarks(width, height),
|
||||
footnote('数据来源:泡泡玛特财报;2020H1/H2非GAAP净利用归母净利替代 | @思考的Joey'),
|
||||
footnote('数据来源:泡泡玛特财报;2020H1/H2非GAAP净利用归母净利替代 | @Joey知行记'),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -76,7 +76,7 @@ export function inventoryBalanceChart(rows, { width, height, view = 'all' }) {
|
||||
],
|
||||
graphic: [
|
||||
...watermarks(width, height),
|
||||
footnote('数据来源:泡泡玛特历年合并资产负债表;存货为报告期末时点数 | @思考的Joey'),
|
||||
footnote('数据来源:泡泡玛特历年合并资产负债表;存货为报告期末时点数 | @Joey知行记'),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { col } from '../csv.js';
|
||||
import {
|
||||
PALETTE, FONT, axisCommon, categoryAxis, titleStyle, legendStyle,
|
||||
financialViewDisclosure, financialViewLabel, shortStandalonePeriod,
|
||||
financialViewDisclosure, financialViewLabel, shortStandalonePeriod, niceAxisMax,
|
||||
lineSeries, footnote, watermarks, LAYOUT,
|
||||
} from '../theme.js';
|
||||
|
||||
@@ -39,6 +39,10 @@ export function ipChart(rows, { width, height, view = 'all' }) {
|
||||
});
|
||||
});
|
||||
|
||||
// 轴上限随数据走(THE MONSTERS 2025 已超 140 亿),写死会把爆款折线顶出绘图区
|
||||
const values = rows.flatMap((row) => periods.map((p) => row[p])).filter(Number.isFinite);
|
||||
const axisMax = niceAxisMax(Math.max(...values));
|
||||
|
||||
return {
|
||||
backgroundColor: '#fff',
|
||||
title: {
|
||||
@@ -74,13 +78,13 @@ export function ipChart(rows, { width, height, view = 'all' }) {
|
||||
nameGap: 52,
|
||||
nameTextStyle: { color: PALETTE.grey, fontSize: 13, fontFamily: FONT },
|
||||
min: 0,
|
||||
max: 100,
|
||||
interval: 20,
|
||||
max: axisMax,
|
||||
interval: axisMax % 4 === 0 ? axisMax / 4 : axisMax / 5,
|
||||
},
|
||||
series,
|
||||
graphic: [
|
||||
...watermarks(width, height),
|
||||
footnote('数据来源:泡泡玛特财报 | @思考的Joey'),
|
||||
footnote('数据来源:泡泡玛特财报 | @Joey知行记'),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -143,7 +143,7 @@ export function ppmtIpExMonstersChart(rows, { width, height, view = 'all' }) {
|
||||
],
|
||||
graphic: [
|
||||
...watermarks(width, height),
|
||||
footnote('除 THE MONSTERS 外其余 IP 合计;同比对去年同半年 | @思考的Joey'),
|
||||
footnote('除 THE MONSTERS 外其余 IP 合计;同比对去年同半年 | @Joey知行记'),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
/**
|
||||
* 泡泡玛特主要 IP 收入占比(2026H1)
|
||||
* 饼图(非环图),图例放在右侧:每项上行 IP 名、下行小字显示收入和占比。
|
||||
* 扇区外直接标注 IP 名 + 数值(两行)。源:ppmt.xlsx「IP收入和占比」2026H1 列,
|
||||
* 扇区外直接标注 IP 名 + 数值/占比(两行),IP 名与扇区同色。源:ppmt.xlsx「IP收入和占比」2026H1 列,
|
||||
* HACIPUPU、BUNNY、PUCKY 等该期无数据的 IP 不列入。
|
||||
* 占比为衍生指标:各 IP 收入 ÷ 全部列入 IP 收入合计(源表占比列未填)。
|
||||
*
|
||||
@@ -29,18 +29,28 @@ const COLORS = [
|
||||
];
|
||||
|
||||
export function ppmtIpShareChart(rows, { width, height }) {
|
||||
const data = rows.map((row, i) => ({
|
||||
name: row.ip,
|
||||
value: row.revenue,
|
||||
itemStyle: { color: COLORS[i % COLORS.length] },
|
||||
}));
|
||||
const data = rows.map((row, i) => {
|
||||
const color = COLORS[i % COLORS.length];
|
||||
return {
|
||||
name: row.ip,
|
||||
value: row.revenue,
|
||||
itemStyle: { color },
|
||||
// 逐扇区覆盖标签 rich:IP 名与所在区块同色,其余样式沿用系列级配置
|
||||
label: {
|
||||
rich: {
|
||||
name: { fontFamily: FONT, fontSize: 12, fontWeight: 600, color, lineHeight: 16 },
|
||||
value: { fontFamily: FONT, fontSize: 11, color: PALETTE.grey, lineHeight: 15 },
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
const total = data.reduce((s, d) => s + d.value, 0);
|
||||
const byName = Object.fromEntries(data.map((d) => [d.name, d]));
|
||||
|
||||
// 把「饼图 + 两侧外标签」当成一个整体,与图例并成一组居中摆放:
|
||||
// 图例紧跟在右侧标签外缘之后,不会被推到画布边缘留下大片空白
|
||||
const LEGEND_WIDTH = 152; // 最宽一行「17.7 亿元 · 10.3%」约 120px + 色块
|
||||
const LEGEND_GAP = 24; // 右侧标签外缘 → 图例
|
||||
const LEGEND_GAP = 56; // 右侧标签外缘 → 图例;最长数值行会侵入该间隙,实测 24px 太挤
|
||||
// 左右标签占位不对称:左侧文字向左延伸(text-anchor: end)要占满,
|
||||
// 右侧只需容下引导线 + 首行文字起段,留太多会在图例前空出一大块
|
||||
const LABEL_LEFT = 135;
|
||||
@@ -105,7 +115,7 @@ export function ppmtIpShareChart(rows, { width, height }) {
|
||||
data,
|
||||
label: {
|
||||
show: true,
|
||||
formatter: (p) => `{name|${p.name}}\n{value|${p.value.toFixed(1)} 亿元}`,
|
||||
formatter: (p) => `{name|${p.name}}\n{value|${p.value.toFixed(1)} 亿元 / ${p.percent.toFixed(1)}%}`,
|
||||
rich: {
|
||||
name: { fontFamily: FONT, fontSize: 12, color: PALETTE.ink, fontWeight: 600, lineHeight: 16 },
|
||||
value: { fontFamily: FONT, fontSize: 11, color: PALETTE.grey, lineHeight: 15 },
|
||||
@@ -120,7 +130,7 @@ export function ppmtIpShareChart(rows, { width, height }) {
|
||||
],
|
||||
graphic: [
|
||||
...watermarks(width, height),
|
||||
footnote('HACIPUPU、BUNNY、PUCKY 等 IP 2026H1 无收入数据,未列入 | @思考的Joey'),
|
||||
footnote('HACIPUPU、BUNNY、PUCKY 等 IP 2026H1 无收入数据,未列入 | @Joey知行记'),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {
|
||||
PALETTE, FONT, axisCommon, categoryAxis, titleStyle,
|
||||
financialViewDisclosure, financialViewLabel, shortStandalonePeriod,
|
||||
standardLegend, barSeries, footnote, watermarks, LAYOUT,
|
||||
standardLegend, barSeries, fanTinyLabelOffsets, stackedSharePoint, footnote, watermarks, LAYOUT,
|
||||
} from '../theme.js';
|
||||
|
||||
/**
|
||||
@@ -40,8 +40,14 @@ export function ppmtIpShareHistoryChart(rows, { width, height, view = 'all' }) {
|
||||
const periods = Object.keys(rows[0]).filter((k) => k !== 'ip');
|
||||
const names = rows.map((r) => r.ip);
|
||||
const totals = periods.map((p) => rows.reduce((s, r) => s + r[p], 0));
|
||||
// 归一化:各 IP 占当期合计的比重,逐期加总恰为 100%
|
||||
const toPct = (r) => periods.map((p, i) => (r[p] / totals[i]) * 100);
|
||||
// 归一化:各 IP 占当期合计的比重,逐期加总恰为 100%。
|
||||
// 转成「期间 × 系列」的行结构,供 fanTinyLabelOffsets 逐柱计算矮段偏移。
|
||||
const pctRows = periods.map((p, i) => Object.fromEntries(
|
||||
rows.map((r) => [r.ip, (r[p] / totals[i]) * 100]),
|
||||
));
|
||||
// 绘图区像素高:画布高 − grid top 145 − grid bottom 70;轴固定 0–100%
|
||||
const pxPerUnit = (height - LAYOUT.gridTop - 70) / 100;
|
||||
const rowOffsets = fanTinyLabelOffsets(pctRows, names, pxPerUnit);
|
||||
|
||||
return {
|
||||
backgroundColor: '#fff',
|
||||
@@ -83,9 +89,14 @@ export function ppmtIpShareHistoryChart(rows, { width, height, view = 'all' }) {
|
||||
interval: 20,
|
||||
axisLabel: { ...axisCommon.axisLabel, formatter: '{value}%' },
|
||||
},
|
||||
series: rows.map((r) => barSeries({
|
||||
// 每个非零占比都要展示:塞得进段内的段内白字,矮段外置到柱体右侧(深字白描边)
|
||||
series: rows.map((r, ci) => barSeries({
|
||||
name: r.ip,
|
||||
data: toPct(r),
|
||||
data: pctRows.map((pr, ri) => stackedSharePoint(pr[r.ip], {
|
||||
pxPerUnit,
|
||||
labelColor: '#fff',
|
||||
dy: rowOffsets[ri][ci] ?? 0,
|
||||
})),
|
||||
color: COLORS[r.ip] ?? PALETTE.grey,
|
||||
barWidth: 40,
|
||||
stack: 'share',
|
||||
@@ -96,14 +107,14 @@ export function ppmtIpShareHistoryChart(rows, { width, height, view = 'all' }) {
|
||||
fontSize: 11,
|
||||
fontFamily: FONT,
|
||||
fontWeight: 500,
|
||||
// 低于 4% 的段塞不进字,留空靠 tooltip 读数
|
||||
formatter: (p) => (p.value >= 4 ? `${p.value.toFixed(1)}%` : ''),
|
||||
overflow: 'none',
|
||||
formatter: (p) => `${p.value.toFixed(1)}%`,
|
||||
},
|
||||
emphasis: { focus: 'series' },
|
||||
})),
|
||||
graphic: [
|
||||
...watermarks(width, height),
|
||||
footnote('其他IP(含已停单列)= Zsiga、HACIPUPU、BUNNY、PUCKY、小甜豆等 | @思考的Joey'),
|
||||
footnote('其他IP(含已停单列)= Zsiga、HACIPUPU、BUNNY、PUCKY、小甜豆等 | @Joey知行记'),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -77,7 +77,7 @@ export function liquidityWorkingCapitalChart(rows, { width, height, view = 'all'
|
||||
],
|
||||
graphic: [
|
||||
...watermarks(width, height),
|
||||
footnote('数据来源:泡泡玛特合并资产负债表 | @思考的Joey'),
|
||||
footnote('数据来源:泡泡玛特合并资产负债表 | @Joey知行记'),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -110,7 +110,7 @@ export function ppmtMarginsChart(rows, { width, height, view = 'all' }) {
|
||||
...watermarks(width, height),
|
||||
footnote(
|
||||
'毛利率/费用率取自利润表;非国际归母净利率=非GAAP净利÷营业收入,' +
|
||||
'2020H1/H2非GAAP缺失用归母净利替代 | @思考的Joey'
|
||||
'2020H1/H2非GAAP缺失用归母净利替代 | @Joey知行记'
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
@@ -123,7 +123,7 @@ export function profitChart(rows, { width, height, view = 'all' }) {
|
||||
],
|
||||
graphic: [
|
||||
...watermarks(width, height),
|
||||
footnote('数据来源:泡泡玛特财报;H2为单独半年 | @思考的Joey'),
|
||||
footnote('数据来源:泡泡玛特财报;H2为单独半年 | @Joey知行记'),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,126 +0,0 @@
|
||||
import {
|
||||
PALETTE, FONT, titleStyle, footnote, watermarks, legendStyle,
|
||||
LAYOUT, } from '../theme.js';
|
||||
|
||||
/**
|
||||
* 泡泡玛特区域渠道分布图(2026H1)
|
||||
* 使用交错正负轴展示不同区域和渠道的收入
|
||||
*/
|
||||
export function regionChannelChart(rows, { width, height }) {
|
||||
// 数据结构重组:按区域分组
|
||||
const regions = ['中国', '海外', '亚太', '美洲', '欧洲及其他'];
|
||||
const channels = ['线下渠道', '线上渠道', '批发及其他'];
|
||||
|
||||
// 构建数据映射
|
||||
const dataMap = {};
|
||||
rows.forEach(row => {
|
||||
const key = `${row.region}_${row.channel}`;
|
||||
dataMap[key] = row.value;
|
||||
});
|
||||
|
||||
// 为每个渠道构建系列数据
|
||||
const seriesData = channels.map(channel => {
|
||||
return regions.map(region => {
|
||||
const key = `${region}_${channel}`;
|
||||
const value = dataMap[key] || 0;
|
||||
// 线上渠道显示为负值(左侧),其他为正值(右侧)
|
||||
return channel === '线上渠道' ? -value : value;
|
||||
});
|
||||
});
|
||||
|
||||
// 配色
|
||||
const channelColors = {
|
||||
'线下渠道': PALETTE.navy,
|
||||
'线上渠道': PALETTE.coral,
|
||||
'批发及其他': PALETTE.sage,
|
||||
};
|
||||
|
||||
return {
|
||||
backgroundColor: '#fff',
|
||||
title: {
|
||||
...titleStyle,
|
||||
text: '泡泡玛特区域渠道分布(2026H1)',
|
||||
subtext: '左侧:线上渠道 | 右侧:线下渠道、批发及其他,单位:亿元',
|
||||
top: 18,
|
||||
},
|
||||
legend: {
|
||||
...legendStyle,
|
||||
data: channels,
|
||||
top: LAYOUT.legendTop,
|
||||
itemGap: 32,
|
||||
textStyle: { fontFamily: FONT, fontSize: 13 },
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: { type: 'shadow' },
|
||||
textStyle: { fontFamily: FONT },
|
||||
formatter: (params) => {
|
||||
let result = `<b>${params[0].axisValue}</b><br/>`;
|
||||
params.forEach(p => {
|
||||
const value = Math.abs(p.value);
|
||||
result += `${p.marker}${p.seriesName}: ${value.toFixed(2)} 亿元<br/>`;
|
||||
});
|
||||
return result;
|
||||
},
|
||||
},
|
||||
grid: {
|
||||
left: 100,
|
||||
right: 100,
|
||||
top: 145,
|
||||
bottom: 90,
|
||||
containLabel: false,
|
||||
},
|
||||
xAxis: {
|
||||
type: 'value',
|
||||
position: 'top',
|
||||
splitLine: {
|
||||
show: true,
|
||||
lineStyle: { color: '#e8e8e8', width: 1 }
|
||||
},
|
||||
axisLine: { show: false },
|
||||
axisTick: { show: false },
|
||||
axisLabel: {
|
||||
formatter: (value) => Math.abs(value),
|
||||
fontFamily: FONT,
|
||||
fontSize: 12,
|
||||
color: '#8c8c8c',
|
||||
},
|
||||
},
|
||||
yAxis: {
|
||||
type: 'category',
|
||||
data: regions,
|
||||
axisLine: { show: false },
|
||||
axisTick: { show: false },
|
||||
axisLabel: {
|
||||
fontFamily: FONT,
|
||||
fontSize: 13,
|
||||
color: '#333',
|
||||
fontWeight: '500',
|
||||
},
|
||||
inverse: false,
|
||||
},
|
||||
series: channels.map((channel, idx) => ({
|
||||
name: channel,
|
||||
type: 'bar',
|
||||
stack: channel === '线上渠道' ? 'left' : 'right',
|
||||
data: seriesData[idx],
|
||||
itemStyle: { color: channelColors[channel] },
|
||||
label: {
|
||||
show: true,
|
||||
position: channel === '线上渠道' ? 'left' : 'right',
|
||||
formatter: (params) => {
|
||||
const value = Math.abs(params.value);
|
||||
return value > 0 ? value.toFixed(1) : '';
|
||||
},
|
||||
fontFamily: FONT,
|
||||
fontSize: 11,
|
||||
color: '#333',
|
||||
},
|
||||
barWidth: 22,
|
||||
})),
|
||||
graphic: [
|
||||
...watermarks(width, height),
|
||||
footnote('数据来源:泡泡玛特财报 | @思考的Joey'),
|
||||
],
|
||||
};
|
||||
}
|
||||
@@ -111,7 +111,7 @@ export function regionChannelYoyTimeseriesChart(rows, { width, height }) {
|
||||
}],
|
||||
graphic: [
|
||||
...watermarks(width, height),
|
||||
footnote('数据来源:泡泡玛特财报;早期海外为合并口径 | @思考的Joey'),
|
||||
footnote('数据来源:泡泡玛特财报;早期海外为合并口径 | @Joey知行记'),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -105,7 +105,7 @@ export function regionShareChart(rows, { width, height, view = 'all' }) {
|
||||
})),
|
||||
graphic: [
|
||||
...watermarks(width, height),
|
||||
footnote('数据来源:泡泡玛特财报;中国含港澳台\n2024H1为按2025年中报披露的分地区同比还原;H2为单半年 | @思考的Joey'),
|
||||
footnote('数据来源:泡泡玛特财报;中国含港澳台\n2024H1为按2025年中报披露的分地区同比还原;H2为单半年 | @Joey知行记'),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -120,7 +120,7 @@ export function regionChannelYoyChart(rows, { width, height }) {
|
||||
})),
|
||||
graphic: [
|
||||
...watermarks(width, height),
|
||||
footnote('数据来源:泡泡玛特财报(2026H1 vs 2025H1同比)| @思考的Joey'),
|
||||
footnote('数据来源:泡泡玛特财报(2026H1 vs 2025H1同比)| @Joey知行记'),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -4,11 +4,13 @@ import {
|
||||
financialViewLabel, barSeries, lineSeries, footnote, watermarks, LAYOUT,
|
||||
} from '../theme.js';
|
||||
|
||||
// 同比虚线用柱色的同色系深色变体:压在柱体和彼此之间都能区分,色相仍与柱对应
|
||||
const REGIONS = [
|
||||
{ key: 'china', name: '中国(含港澳台)', color: '#3F7FBE' },
|
||||
{ key: 'apac', name: '亚太', color: '#36A28F' },
|
||||
{ key: 'americas', name: '美洲', color: '#E4A349' },
|
||||
{ key: 'europe_other', name: '欧洲及其他', color: PALETTE.brown },
|
||||
{ key: 'china', name: '中国(含港澳台)', color: '#3F7FBE', lineColor: PALETTE.navy },
|
||||
// 亚太青、美洲金、深棕在色板无对应深色,按柱色加深或取邻近深色
|
||||
{ key: 'apac', name: '亚太', color: '#36A28F', lineColor: '#1F6E5F' },
|
||||
{ key: 'americas', name: '美洲', color: '#E4A349', lineColor: PALETTE.orange },
|
||||
{ key: 'europe_other', name: '欧洲及其他', color: PALETTE.brown, lineColor: '#4A3117' },
|
||||
];
|
||||
|
||||
function yoyByPeriod(rows, key) {
|
||||
@@ -37,7 +39,7 @@ export function retailStoreRevenueChart(rows, { width, height, view = 'all' }) {
|
||||
title: {
|
||||
...titleStyle,
|
||||
text: `泡泡玛特各地区零售店单店收入(${financialViewLabel(view)})`,
|
||||
subtext: '柱状为单店收入;同色虚线为单店收入同比;H2为下半年单期',
|
||||
subtext: '柱状为单店收入;深色虚线为对应柱的单店收入同比;H2为下半年单期',
|
||||
},
|
||||
legend: standardLegend(legendItems, { itemGap: 18 }),
|
||||
grid: { left: 78, right: 42, top: LAYOUT.gridTop, bottom: 90 },
|
||||
@@ -93,18 +95,18 @@ export function retailStoreRevenueChart(rows, { width, height, view = 'all' }) {
|
||||
...lineSeries({
|
||||
name: `${region.name}同比`,
|
||||
data: region.data,
|
||||
color: region.color,
|
||||
color: region.lineColor,
|
||||
width: 2,
|
||||
formatter: (point) => `${point.value.toFixed(0)}%`,
|
||||
}),
|
||||
yAxisIndex: 1,
|
||||
lineStyle: { color: region.color, width: 2, type: 'dashed' },
|
||||
lineStyle: { color: region.lineColor, width: 2, type: 'dashed' },
|
||||
z: 10,
|
||||
})),
|
||||
],
|
||||
graphic: [
|
||||
...watermarks(width, height),
|
||||
footnote('数据来源:泡泡玛特历年公告;收入仅含“零售店”渠道;H2收入=全年−H1\n2024年海外各区域缺少期初店数,相关单店收入留空 | @思考的Joey'),
|
||||
footnote('数据来源:泡泡玛特历年公告;收入仅含“零售店”渠道;H2收入=全年−H1\n2024年海外各区域缺少期初店数,相关单店收入留空 | @Joey知行记'),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -4,9 +4,11 @@ import {
|
||||
financialViewLabel, barSeries, lineSeries, footnote, watermarks, LAYOUT,
|
||||
} from '../theme.js';
|
||||
|
||||
// 同比虚线用柱色的同色系深色变体:压在柱体和彼此之间都能区分,色相仍与柱对应
|
||||
const REGIONS = [
|
||||
{ key: 'china', name: '中国(含港澳台)', color: '#71AD81' },
|
||||
{ key: 'overseas', name: '海外', color: PALETTE.brown },
|
||||
{ key: 'china', name: '中国(含港澳台)', color: '#71AD81', lineColor: PALETTE.green },
|
||||
// 色板没有深棕,按 brown 加深
|
||||
{ key: 'overseas', name: '海外', color: PALETTE.brown, lineColor: '#4A3117' },
|
||||
];
|
||||
|
||||
function yoyByPeriod(rows, key) {
|
||||
@@ -35,7 +37,7 @@ export function robotStoreRevenueChart(rows, { width, height, view = 'all' }) {
|
||||
title: {
|
||||
...titleStyle,
|
||||
text: `泡泡玛特机器人商店单店收入(${financialViewLabel(view)})`,
|
||||
subtext: '柱状为单店收入;同色虚线为单店收入同比;H2为下半年单期',
|
||||
subtext: '柱状为单店收入;深色虚线为对应柱的单店收入同比;H2为下半年单期',
|
||||
},
|
||||
legend: standardLegend(legendItems, { itemGap: 18 }),
|
||||
grid: { left: 78, right: 42, top: LAYOUT.gridTop, bottom: 90 },
|
||||
@@ -91,18 +93,18 @@ export function robotStoreRevenueChart(rows, { width, height, view = 'all' }) {
|
||||
...lineSeries({
|
||||
name: `${region.name}同比`,
|
||||
data: region.data,
|
||||
color: region.color,
|
||||
color: region.lineColor,
|
||||
width: 2,
|
||||
formatter: (point) => `${point.value.toFixed(0)}%`,
|
||||
}),
|
||||
yAxisIndex: 1,
|
||||
lineStyle: { color: region.color, width: 2, type: 'dashed' },
|
||||
lineStyle: { color: region.lineColor, width: 2, type: 'dashed' },
|
||||
z: 10,
|
||||
})),
|
||||
],
|
||||
graphic: [
|
||||
...watermarks(width, height),
|
||||
footnote('数据来源:泡泡玛特历年公告;海外收入=亚太+美洲+欧洲及其他的机器人商店收入;H2收入=全年−H1\n2024H1海外期末机器人商店数未披露,相关单店收入留空 | @思考的Joey'),
|
||||
footnote('数据来源:泡泡玛特历年公告;海外收入=亚太+美洲+欧洲及其他的机器人商店收入;H2收入=全年−H1\n2024H1海外期末机器人商店数未披露,相关单店收入留空 | @Joey知行记'),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -133,7 +133,7 @@ export function sankeyChart(rows, { width, height }) {
|
||||
],
|
||||
graphic: [
|
||||
...watermarks(width, height),
|
||||
footnote('数据来源:泡泡玛特财报 | @思考的Joey'),
|
||||
footnote('数据来源:泡泡玛特财报 | @Joey知行记'),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -128,7 +128,7 @@ export function storesRetailChart(rows, { width, height, view = 'all' }) {
|
||||
],
|
||||
graphic: [
|
||||
...watermarks(width, height),
|
||||
footnote('数据来源:泡泡玛特历年公告;门店数为各期末时点数\n2023年及以前海外合计含港澳台;2024年起港澳台单列,海外=亚太+美洲+欧洲 | @思考的Joey'),
|
||||
footnote('数据来源:泡泡玛特历年公告;门店数为各期末时点数\n2023年及以前海外合计含港澳台;2024年起港澳台单列,海外=亚太+美洲+欧洲 | @Joey知行记'),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
@@ -77,7 +75,7 @@ export function storesRetailShareChart(rows, { width, height, view = 'all' }) {
|
||||
})),
|
||||
graphic: [
|
||||
...watermarks(width, height),
|
||||
footnote('数据来源:泡泡玛特历年公告;门店数为各报告期末时点数\n中国=中国内地+港澳台;其他=全球合计−中国−亚太−美洲−欧洲 | @思考的Joey'),
|
||||
footnote('数据来源:泡泡玛特历年公告;门店数为各报告期末时点数\n中国=中国内地+港澳台;其他=全球合计−中国−亚太−美洲−欧洲 | @Joey知行记'),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -135,8 +135,8 @@ export function storesRobotChart(rows, { width, height, view = 'all' }) {
|
||||
[
|
||||
'数据来源:泡泡玛特历年公告;门店数为各期末时点数',
|
||||
view === 'annual'
|
||||
? '海外合计2023年及以前含港澳台、其后不含 | @思考的Joey'
|
||||
: '海外合计2023年及以前含港澳台、其后不含;2024H1海外数量未披露 | @思考的Joey',
|
||||
? '海外合计2023年及以前含港澳台、其后不含 | @Joey知行记'
|
||||
: '海外合计2023年及以前含港澳台、其后不含;2024H1海外数量未披露 | @Joey知行记',
|
||||
].join('\n'),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -78,7 +78,7 @@ export function workingCapitalDaysChart(rows, { width, height }) {
|
||||
})),
|
||||
graphic: [
|
||||
...watermarks(width, height),
|
||||
footnote('数据来源:泡泡玛特财务指标(合并报表);原始180/360天口径已换算为181/365天 | @思考的Joey'),
|
||||
footnote('数据来源:泡泡玛特财务指标(合并报表);原始180/360天口径已换算为181/365天 | @Joey知行记'),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ export function twoLineChart(rows, { width, height }) {
|
||||
],
|
||||
graphic: [
|
||||
...watermarks(width, height),
|
||||
footnote('示例数据 | @思考的Joey'),
|
||||
footnote('示例数据 | @Joey知行记'),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
+121
@@ -1,17 +1,25 @@
|
||||
import { loadCSV } from './csv.js';
|
||||
import { getChartBuilder } from './charts/index.js';
|
||||
import { downloadCanvas, mergeImages } from './merge.js';
|
||||
import {
|
||||
createAppShell,
|
||||
renderAnchors,
|
||||
renderCards,
|
||||
renderCartBar,
|
||||
renderCompanies,
|
||||
renderTabs,
|
||||
showChartError,
|
||||
showMergePreview,
|
||||
} from './ui.js';
|
||||
|
||||
const instances = new Map();
|
||||
const activeViews = new Map();
|
||||
const chartRenderVersions = new Map();
|
||||
// 合并队列:instanceId → { instanceId, title, filename, dataUrl }
|
||||
// dataUrl 是加购(及后续每次重渲染)时的快照,切换 tab 后实例被 dispose 也能照常导出
|
||||
const cart = new Map();
|
||||
let cartMode = 'pairs';
|
||||
let closePreview;
|
||||
let manifest;
|
||||
let shell;
|
||||
let state;
|
||||
@@ -123,6 +131,7 @@ async function renderChart(chart, version, chartVersion) {
|
||||
instances.set(chart.instanceId, instance);
|
||||
const { width, height } = el.getBoundingClientRect();
|
||||
instance.setOption(getChartBuilder(chart.key)(rows, { width, height, view }), true);
|
||||
captureSnapshot(chart);
|
||||
} catch (error) {
|
||||
if (version === renderVersion) showChartError(chart.instanceId, error.message);
|
||||
console.error(`渲染图表失败:${chart.id}`, error);
|
||||
@@ -157,6 +166,116 @@ function exportPNG(chartId, filename) {
|
||||
anchor.click();
|
||||
}
|
||||
|
||||
/** 图表在合并队列中时,每次重渲染(切换视图、重载数据、resize)都刷新快照,保证所见即所得 */
|
||||
function captureSnapshot(chart) {
|
||||
if (!cart.has(chart.instanceId)) return;
|
||||
const instance = instances.get(chart.instanceId);
|
||||
if (!instance) return;
|
||||
cart.get(chart.instanceId).dataUrl = instance.getDataURL({ type: 'png', pixelRatio: 2, backgroundColor: '#fff' });
|
||||
}
|
||||
|
||||
function toggleCart(chart) {
|
||||
if (cart.delete(chart.instanceId)) {
|
||||
updateCartButton(chart.instanceId);
|
||||
} else {
|
||||
const instance = instances.get(chart.instanceId);
|
||||
if (!instance) return;
|
||||
cart.set(chart.instanceId, {
|
||||
instanceId: chart.instanceId,
|
||||
title: chart.title,
|
||||
filename: chart.filename || chart.title,
|
||||
dataUrl: instance.getDataURL({ type: 'png', pixelRatio: 2, backgroundColor: '#fff' }),
|
||||
});
|
||||
updateCartButton(chart.instanceId);
|
||||
}
|
||||
renderCart();
|
||||
}
|
||||
|
||||
function updateCartButton(instanceId) {
|
||||
const btn = document.querySelector(`[data-cart-id="${instanceId}"]`);
|
||||
if (!btn) return;
|
||||
const inCart = cart.has(instanceId);
|
||||
btn.dataset.inCart = String(inCart);
|
||||
btn.textContent = inCart ? '已加入 ✓' : '➕ 加入合并';
|
||||
}
|
||||
|
||||
/** Map 保持插入序,删除后按新顺序重建即完成上移/下移 */
|
||||
function moveCartItem(instanceId, delta) {
|
||||
const entries = [...cart.entries()];
|
||||
const from = entries.findIndex(([id]) => id === instanceId);
|
||||
const to = from + delta;
|
||||
if (from < 0 || to < 0 || to >= entries.length) return;
|
||||
[entries[from], entries[to]] = [entries[to], entries[from]];
|
||||
cart.clear();
|
||||
entries.forEach(([id, item]) => cart.set(id, item));
|
||||
renderCart();
|
||||
}
|
||||
|
||||
function clipName(name, max = 24) {
|
||||
return name.length > max ? `${name.slice(0, max)}…` : name;
|
||||
}
|
||||
|
||||
/** 按当前模式把队列合成为一组 { canvas, filename, partial? }:pairs 两两合并,single 全部拼一张 */
|
||||
async function buildCompositions() {
|
||||
const items = [...cart.values()];
|
||||
if (!items.length) return [];
|
||||
if (cartMode === 'single') {
|
||||
return [{
|
||||
canvas: await mergeImages(items.map((item) => item.dataUrl)),
|
||||
filename: `${clipName(items[0].filename)}等${items.length}张合并`,
|
||||
}];
|
||||
}
|
||||
const pairs = [];
|
||||
for (let i = 0; i < items.length; i += 2) {
|
||||
const pair = items.slice(i, i + 2);
|
||||
pairs.push({
|
||||
canvas: await mergeImages(pair.map((item) => item.dataUrl)),
|
||||
filename: pair.map((item) => clipName(item.filename)).join('+'),
|
||||
partial: pair.length === 1,
|
||||
});
|
||||
}
|
||||
return pairs;
|
||||
}
|
||||
|
||||
async function openMergePreview() {
|
||||
const entries = await buildCompositions();
|
||||
if (!entries.length) return;
|
||||
const notice = entries.some((entry) => entry.partial) ? '队列为奇数张,最后一张单独导出' : '';
|
||||
const download = (entry) => downloadCanvas(entry.canvas, entry.filename);
|
||||
closePreview = showMergePreview(entries, {
|
||||
notice,
|
||||
onDownload: download,
|
||||
onDownloadAll: () => entries.forEach((entry, index) => {
|
||||
setTimeout(() => download(entry), index * 350);
|
||||
}),
|
||||
onClose: () => closePreview?.(),
|
||||
});
|
||||
}
|
||||
|
||||
function renderCart() {
|
||||
renderCartBar(shell.cartBar, {
|
||||
items: [...cart.values()],
|
||||
mode: cartMode,
|
||||
onModeChange: (mode) => {
|
||||
cartMode = mode;
|
||||
renderCart();
|
||||
},
|
||||
onMove: moveCartItem,
|
||||
onRemove: (instanceId) => {
|
||||
cart.delete(instanceId);
|
||||
updateCartButton(instanceId);
|
||||
renderCart();
|
||||
},
|
||||
onClear: () => {
|
||||
const ids = [...cart.keys()];
|
||||
cart.clear();
|
||||
ids.forEach(updateCartButton);
|
||||
renderCart();
|
||||
},
|
||||
onPreview: openMergePreview,
|
||||
});
|
||||
}
|
||||
|
||||
function updateHash(nextState) {
|
||||
const hash = serializeHash(nextState);
|
||||
if (window.location.hash === hash) {
|
||||
@@ -189,6 +308,8 @@ function renderCurrent() {
|
||||
});
|
||||
renderCards(shell.cards, charts, activeViews, {
|
||||
onExport: exportPNG,
|
||||
isCarted: (instanceId) => cart.has(instanceId),
|
||||
onToggleCart: toggleCart,
|
||||
onSelectView: (chart, view) => {
|
||||
if (activeViews.get(chart.viewKey) === view) return;
|
||||
activeViews.set(chart.viewKey, view);
|
||||
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* 图片纵向合成 —— 把多张图表 PNG 顺序拼接为一张竖版长图。
|
||||
* 公众号文章在手机上以竖版窗口预览,横版单图显示过小,
|
||||
* 两张上下拼接后(2400×~2800)接近满屏,阅读体验明显更好。
|
||||
* 纯浏览器 Canvas API,无依赖。
|
||||
*/
|
||||
|
||||
const DEFAULTS = { gap: 96, background: '#fff' }; // gap 为设备像素:@2x 导出下 96 = 视觉 48px
|
||||
|
||||
function loadImage(src) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const img = new Image();
|
||||
img.onload = () => resolve(img);
|
||||
img.onerror = () => reject(new Error('图片解码失败'));
|
||||
img.src = src;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 按顺序纵向拼接一组图片 dataURL,返回合成后的 canvas。
|
||||
* 宽度取各图最大值;宽度不一致的图等比缩放对齐(正常流程导出的图宽度一致,不会触发缩放)。
|
||||
*/
|
||||
export async function mergeImages(sources, options = {}) {
|
||||
const { gap, background } = { ...DEFAULTS, ...options };
|
||||
if (!sources.length) throw new Error('没有可合并的图片');
|
||||
|
||||
const images = await Promise.all(sources.map(loadImage));
|
||||
const width = Math.max(...images.map((img) => img.width));
|
||||
const heights = images.map((img) => Math.round(img.height * width / img.width));
|
||||
const height = heights.reduce((sum, h) => sum + h, 0) + gap * (images.length - 1);
|
||||
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.fillStyle = background;
|
||||
ctx.fillRect(0, 0, width, height);
|
||||
|
||||
let y = 0;
|
||||
images.forEach((img, index) => {
|
||||
ctx.drawImage(img, 0, y, width, heights[index]);
|
||||
y += heights[index] + gap;
|
||||
});
|
||||
return canvas;
|
||||
}
|
||||
|
||||
/** canvas → PNG Blob 触发下载(Blob URL 比超长 dataURL 下载更稳) */
|
||||
export function downloadCanvas(canvas, filename) {
|
||||
canvas.toBlob((blob) => {
|
||||
const url = URL.createObjectURL(blob);
|
||||
const anchor = document.createElement('a');
|
||||
anchor.href = url;
|
||||
anchor.download = filename.endsWith('.png') ? filename : `${filename}.png`;
|
||||
anchor.click();
|
||||
setTimeout(() => URL.revokeObjectURL(url), 10_000);
|
||||
}, 'image/png');
|
||||
}
|
||||
+72
-2
@@ -1,9 +1,9 @@
|
||||
/**
|
||||
* 方木风格主题 —— 统一的配色、字体、坐标轴、水印
|
||||
* Joey 风格主题 —— 统一的配色、字体、坐标轴、水印
|
||||
* 所有图表共用这一份配置,保证视觉一致性
|
||||
*/
|
||||
|
||||
export const WATERMARK_TEXT = '@思考的Joey';
|
||||
export const WATERMARK_TEXT = '@Joey知行记';
|
||||
|
||||
// 字体名用单引号:SVG 渲染时字符串会被塞进 style="..." 属性,
|
||||
// 用双引号会提前闭合属性、生成非法 SVG(canvas 渲染看不出来,导出 SVG 时才炸)
|
||||
@@ -161,6 +161,15 @@ export const LAYOUT = {
|
||||
gridTop: 145, // 绘图区顶部边距(图例到绘图区约 60px)
|
||||
};
|
||||
|
||||
/** 数值轴整齐上限候选:轴 max 从中取第一个能容纳数据的刻度 */
|
||||
export const NICE_AXIS_MAX = [20, 25, 50, 60, 80, 100, 120, 150, 200, 250, 300, 400, 500, 600, 800, 1000];
|
||||
|
||||
/** 按数据最大值取整齐轴上限;超出候选表尾时按 100 的整数倍向上取整并留 20% 余量 */
|
||||
export function niceAxisMax(dataMax) {
|
||||
return NICE_AXIS_MAX.find((max) => max >= dataMax * 1.02)
|
||||
?? Math.ceil((dataMax * 1.2) / 100) * 100;
|
||||
}
|
||||
|
||||
// 统一图例图标宽度,同时保留 ECharts 按系列类型生成的形状。
|
||||
export const LEGEND_ITEM_WIDTH = 20;
|
||||
|
||||
@@ -291,6 +300,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 {
|
||||
|
||||
@@ -28,9 +28,11 @@ export function createAppShell(root, { onReload }) {
|
||||
content.append(tabs, cards);
|
||||
const chartNav = element('aside', 'sidebar chart-sidebar');
|
||||
layout.append(companyNav, content, chartNav);
|
||||
root.append(layout, reloadButton);
|
||||
const cartBar = element('div', 'cart-bar');
|
||||
cartBar.hidden = true;
|
||||
root.append(layout, reloadButton, cartBar);
|
||||
|
||||
return { companyNav, tabs, cards, chartNav };
|
||||
return { companyNav, tabs, cards, chartNav, cartBar };
|
||||
}
|
||||
|
||||
export function renderCompanies(container, companies, activeCompanyId, onSelect) {
|
||||
@@ -57,7 +59,7 @@ export function renderTabs(container, tabs, activeTabId, onSelect) {
|
||||
});
|
||||
}
|
||||
|
||||
export function renderCards(container, charts, activeViews, { onExport, onSelectView }) {
|
||||
export function renderCards(container, charts, activeViews, { onExport, isCarted, onToggleCart, onSelectView }) {
|
||||
container.replaceChildren();
|
||||
if (!charts.length) {
|
||||
container.append(element('p', 'empty-state', '该分类暂时没有图表。'));
|
||||
@@ -92,7 +94,13 @@ export function renderCards(container, charts, activeViews, { onExport, onSelect
|
||||
bar.append(switcher);
|
||||
}
|
||||
|
||||
bar.append(button('导出 PNG', {
|
||||
const cartButton = button(isCarted?.(chart.instanceId) ? '已加入 ✓' : '➕ 加入合并', {
|
||||
onClick: () => onToggleCart?.(chart),
|
||||
className: 'cart-button',
|
||||
});
|
||||
cartButton.dataset.cartId = chart.instanceId;
|
||||
cartButton.dataset.inCart = String(Boolean(isCarted?.(chart.instanceId)));
|
||||
bar.append(cartButton, button('导出 PNG', {
|
||||
onClick: () => onExport(chart.instanceId, chart.filename || chart.title),
|
||||
className: 'export-button',
|
||||
}));
|
||||
@@ -121,3 +129,95 @@ export function showChartError(chartId, message) {
|
||||
if (!chartEl) return;
|
||||
chartEl.replaceChildren(element('div', 'chart-error', message));
|
||||
}
|
||||
|
||||
const CART_MODES = [
|
||||
['pairs', '两两合并'],
|
||||
['single', '合并为一张'],
|
||||
];
|
||||
|
||||
export function renderCartBar(container, { items = [], mode, onModeChange, onMove, onRemove, onClear, onPreview }) {
|
||||
container.replaceChildren();
|
||||
if (!items.length) {
|
||||
container.hidden = true;
|
||||
return;
|
||||
}
|
||||
container.hidden = false;
|
||||
|
||||
container.append(element('span', 'cart-label', `合并队列 ${items.length}`));
|
||||
|
||||
const chips = element('div', 'cart-chips');
|
||||
items.forEach((item, index) => {
|
||||
const chip = element('div', 'cart-chip');
|
||||
chip.title = item.title;
|
||||
const up = button('↑', { onClick: () => onMove?.(item.instanceId, -1), className: 'cart-chip-button' });
|
||||
up.disabled = index === 0;
|
||||
const down = button('↓', { onClick: () => onMove?.(item.instanceId, 1), className: 'cart-chip-button' });
|
||||
down.disabled = index === items.length - 1;
|
||||
chip.append(
|
||||
element('span', 'cart-chip-name', item.title),
|
||||
up,
|
||||
down,
|
||||
button('✕', { onClick: () => onRemove?.(item.instanceId), className: 'cart-chip-button' }),
|
||||
);
|
||||
chips.append(chip);
|
||||
});
|
||||
container.append(chips);
|
||||
|
||||
container.append(element('span', 'cart-divider'));
|
||||
|
||||
const modes = element('div', 'cart-modes');
|
||||
CART_MODES.forEach(([value, label]) => {
|
||||
modes.append(button(label, {
|
||||
active: mode === value,
|
||||
onClick: () => onModeChange?.(value),
|
||||
className: 'cart-mode-button',
|
||||
}));
|
||||
});
|
||||
container.append(modes);
|
||||
|
||||
container.append(button('预览导出', { onClick: () => onPreview?.(), className: 'cart-export-button' }));
|
||||
container.append(button('清空', { onClick: () => onClear?.(), className: 'cart-clear-button' }));
|
||||
}
|
||||
|
||||
export function showMergePreview(entries, { notice = '', onDownload, onDownloadAll, onClose }) {
|
||||
const overlay = element('div', 'modal-overlay');
|
||||
const modal = element('div', 'modal');
|
||||
|
||||
const head = element('div', 'modal-head');
|
||||
head.append(element('h2', 'modal-title', `合并预览 · ${entries.length} 张`));
|
||||
if (notice) head.append(element('span', 'modal-notice', notice));
|
||||
head.append(button('✕', { onClick: onClose, className: 'modal-close' }));
|
||||
|
||||
const body = element('div', 'modal-body');
|
||||
entries.forEach((entry) => {
|
||||
const item = element('div', 'preview-item');
|
||||
entry.canvas.className = 'preview-canvas';
|
||||
item.append(entry.canvas);
|
||||
item.append(button(`⬇ 下载:${entry.filename}.png`, {
|
||||
onClick: () => onDownload?.(entry),
|
||||
className: 'export-button',
|
||||
}));
|
||||
body.append(item);
|
||||
});
|
||||
|
||||
const foot = element('div', 'modal-foot');
|
||||
foot.append(button('全部下载', { onClick: () => onDownloadAll?.(), className: 'primary-button' }));
|
||||
foot.append(button('取消', { onClick: onClose, className: 'export-button' }));
|
||||
|
||||
modal.append(head, body, foot);
|
||||
overlay.append(modal);
|
||||
overlay.addEventListener('click', (event) => {
|
||||
if (event.target === overlay) onClose?.();
|
||||
});
|
||||
document.addEventListener('keydown', onKeydown);
|
||||
document.body.append(overlay);
|
||||
|
||||
function onKeydown(event) {
|
||||
if (event.key === 'Escape') onClose?.();
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('keydown', onKeydown);
|
||||
overlay.remove();
|
||||
};
|
||||
}
|
||||
|
||||
+56
-3
@@ -18,7 +18,6 @@
|
||||
"ppmt-ip-share-history": { "key": "ppmt-ip-share-history", "title": "各 IP 半年收入占比", "defaultView": "all", "views": { "all": "data/ppmt/ppmt_ip_share_history.csv", "annual": "data/ppmt/ppmt_ip_share_history_annual.csv", "halfyear": "data/ppmt/ppmt_ip_share_history_halfyear.csv" }, "height": 700, "filename": "泡泡玛特各IP半年收入占比" },
|
||||
"ppmt-ip-ex-monsters": { "key": "ppmt-ip-ex-monsters", "title": "除 THE MONSTERS 外的 IP 收入及同比", "defaultView": "all", "views": { "all": "data/ppmt/ppmt_ip_ex_monsters.csv", "annual": "data/ppmt/ppmt_ip_ex_monsters_annual.csv", "halfyear": "data/ppmt/ppmt_ip_ex_monsters_halfyear.csv" }, "height": 700, "filename": "泡泡玛特除MONSTERS外IP收入及同比" },
|
||||
"ppmt-category": { "key": "ppmt-category", "title": "品类收入结构", "defaultView": "all", "views": { "all": "data/ppmt/ppmt_category.csv", "annual": "data/ppmt/ppmt_category_annual.csv", "halfyear": "data/ppmt/ppmt_category_halfyear.csv" }, "height": 680, "filename": "泡泡玛特品类结构" },
|
||||
"ppmt-region-channel": { "key": "ppmt-region-channel", "title": "区域渠道分布", "csv": "data/ppmt/ppmt_region_channel_2026h1.csv", "height": 680, "filename": "泡泡玛特区域渠道分布2026H1" },
|
||||
"ppmt-region-yoy": { "key": "ppmt-region-yoy", "title": "区域渠道同比增速", "csv": "data/ppmt/ppmt_region_channel_yoy.csv", "height": 620, "filename": "泡泡玛特区域渠道同比增速" },
|
||||
"ppmt-region-yoy-timeseries": { "key": "ppmt-region-yoy-timeseries", "title": "区域渠道同比增速趋势", "csv": "data/ppmt/ppmt_region_channel_yoy_timeseries.csv", "height": 760, "filename": "泡泡玛特区域渠道同比增速趋势" },
|
||||
"ppmt-cash-conversion": { "key": "ppmt-cash-conversion", "title": "经营现金流与净利润", "defaultView": "all", "views": { "all": "data/ppmt/ppmt_cash_conversion.csv", "annual": "data/ppmt/ppmt_cash_conversion_annual.csv", "halfyear": "data/ppmt/ppmt_cash_conversion_halfyear.csv" }, "height": 700, "filename": "泡泡玛特经营现金流与净利润" },
|
||||
@@ -28,6 +27,14 @@
|
||||
"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-channel-amount-china": { "key": "ppmt-channel-amount-china", "title": "中国区渠道收入金额", "defaultView": "all", "views": { "all": "data/ppmt/ppmt_channel_amount_china.csv", "annual": "data/ppmt/ppmt_channel_amount_china_annual.csv", "halfyear": "data/ppmt/ppmt_channel_amount_china_halfyear.csv" }, "height": 680, "filename": "泡泡玛特中国区渠道收入金额" },
|
||||
"ppmt-channel-amount-apac": { "key": "ppmt-channel-amount-apac", "title": "亚太区渠道收入金额", "defaultView": "all", "views": { "all": "data/ppmt/ppmt_channel_amount_apac.csv", "annual": "data/ppmt/ppmt_channel_amount_apac_annual.csv", "halfyear": "data/ppmt/ppmt_channel_amount_apac_halfyear.csv" }, "height": 680, "filename": "泡泡玛特亚太区渠道收入金额" },
|
||||
"ppmt-channel-amount-americas": { "key": "ppmt-channel-amount-americas", "title": "美洲区渠道收入金额", "defaultView": "all", "views": { "all": "data/ppmt/ppmt_channel_amount_americas.csv", "annual": "data/ppmt/ppmt_channel_amount_americas_annual.csv", "halfyear": "data/ppmt/ppmt_channel_amount_americas_halfyear.csv" }, "height": 680, "filename": "泡泡玛特美洲区渠道收入金额" },
|
||||
"ppmt-channel-amount-europe": { "key": "ppmt-channel-amount-europe", "title": "欧洲及其他区渠道收入金额", "defaultView": "all", "views": { "all": "data/ppmt/ppmt_channel_amount_europe.csv", "annual": "data/ppmt/ppmt_channel_amount_europe_annual.csv", "halfyear": "data/ppmt/ppmt_channel_amount_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": "泡泡玛特各地区零售店单店收入" },
|
||||
@@ -36,15 +43,61 @@
|
||||
"ppmt-region-share": { "key": "ppmt-region-share", "title": "分地区营收占比", "defaultView": "all", "views": { "all": "data/ppmt/ppmt_region_share.csv", "annual": "data/ppmt/ppmt_region_share_annual.csv", "halfyear": "data/ppmt/ppmt_region_share_halfyear.csv" }, "height": 680, "filename": "泡泡玛特分地区营收占比" }
|
||||
},
|
||||
"tabs": [
|
||||
{ "id": "overview", "name": "概览", "chartRefs": ["ppmt-growth", "ppmt-profit", "ppmt-ip-share", "ppmt-region-channel"] },
|
||||
{ "id": "overview", "name": "概览", "chartRefs": ["ppmt-growth", "ppmt-profit", "ppmt-ip-share"] },
|
||||
{ "id": "profitability", "name": "盈利能力", "chartRefs": ["ppmt-growth", "ppmt-core-profit", "ppmt-core-margin", "ppmt-bridge", "ppmt-sankey", "ppmt-cash-conversion", "ppmt-cashflow-composition"] },
|
||||
{ "id": "expense", "name": "费用率", "chartRefs": ["ppmt-margins", "ppmt-expense-structure"] },
|
||||
{ "id": "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-yoy", "ppmt-region-yoy-timeseries", "ppmt-channel-revenue", "ppmt-channel-share", "ppmt-channel-share-china", "ppmt-channel-amount-china", "ppmt-channel-share-apac", "ppmt-channel-amount-apac", "ppmt-channel-share-americas", "ppmt-channel-amount-americas", "ppmt-channel-share-europe", "ppmt-channel-amount-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": "moutai",
|
||||
"name": "贵州茅台",
|
||||
"chartCatalog": {
|
||||
"moutai-profitability-annual": { "key": "moutai-profitability-annual", "title": "年度盈利能力", "csv": "data/moutai/moutai_profitability_annual.csv", "height": 680, "filename": "贵州茅台年度盈利能力" },
|
||||
"moutai-profitability-quarterly": { "key": "moutai-profitability-quarterly", "title": "单季盈利能力", "csv": "data/moutai/moutai_profitability_quarterly.csv", "height": 680, "filename": "贵州茅台单季盈利能力" },
|
||||
"moutai-expense-annual": { "key": "moutai-expense-annual", "title": "年度费用结构", "csv": "data/moutai/moutai_expense_annual.csv", "height": 680, "filename": "贵州茅台年度费用结构" },
|
||||
"moutai-expense-quarterly": { "key": "moutai-expense-quarterly", "title": "单季费用结构", "csv": "data/moutai/moutai_expense_quarterly.csv", "height": 680, "filename": "贵州茅台单季费用结构" },
|
||||
"moutai-balance-annual": { "key": "moutai-balance-annual", "title": "年度资产负债结构", "csv": "data/moutai/moutai_balance_annual.csv", "height": 680, "filename": "贵州茅台年度资产负债结构" },
|
||||
"moutai-balance-quarterly": { "key": "moutai-balance-quarterly", "title": "季度资产负债结构", "csv": "data/moutai/moutai_balance_quarterly.csv", "height": 680, "filename": "贵州茅台季度资产负债结构" },
|
||||
"moutai-cashflow-annual": { "key": "moutai-cashflow-annual", "title": "年度三大现金流", "csv": "data/moutai/moutai_cashflow_annual.csv", "height": 680, "filename": "贵州茅台年度三大现金流" },
|
||||
"moutai-cashflow-quarterly": { "key": "moutai-cashflow-quarterly", "title": "单季三大现金流", "csv": "data/moutai/moutai_cashflow_quarterly.csv", "height": 680, "filename": "贵州茅台单季三大现金流" },
|
||||
"moutai-product-annual": { "key": "moutai-product-annual", "title": "年度产品档次营收", "csv": "data/moutai/moutai_product_revenue_annual.csv", "height": 680, "filename": "贵州茅台年度产品档次营收" },
|
||||
"moutai-channel-annual": { "key": "moutai-channel-annual", "title": "年度销售渠道营收", "csv": "data/moutai/moutai_channel_revenue_annual.csv", "height": 680, "filename": "贵州茅台年度销售渠道营收" },
|
||||
"moutai-classified-product-quarterly": { "key": "moutai-classified-product-quarterly", "title": "单季产品档次营收", "csv": "data/moutai/moutai_classified_revenue_quarterly.csv", "height": 680, "filename": "贵州茅台单季产品档次营收" },
|
||||
"moutai-classified-channel-quarterly": { "key": "moutai-classified-channel-quarterly", "title": "单季销售渠道营收", "csv": "data/moutai/moutai_classified_revenue_quarterly.csv", "height": 680, "filename": "贵州茅台单季销售渠道营收" },
|
||||
"moutai-classified-product-cumulative": { "key": "moutai-classified-product-cumulative", "title": "累计产品档次营收", "csv": "data/moutai/moutai_classified_revenue_cumulative.csv", "height": 680, "filename": "贵州茅台累计产品档次营收" },
|
||||
"moutai-classified-channel-cumulative": { "key": "moutai-classified-channel-cumulative", "title": "累计销售渠道营收", "csv": "data/moutai/moutai_classified_revenue_cumulative.csv", "height": 680, "filename": "贵州茅台累计销售渠道营收" },
|
||||
"moutai-revenue-composition-halfyear": { "key": "moutai-revenue-composition-halfyear", "title": "半年度累计营收构成", "csv": "data/moutai/moutai_revenue_composition_halfyear.csv", "height": 680, "filename": "贵州茅台半年度累计营收构成" },
|
||||
"moutai-liquor-industry-output": { "key": "moutai-liquor-industry-output", "title": "全国白酒产量", "csv": "data/moutai/moutai_liquor_industry_output.csv", "height": 680, "filename": "全国白酒产量" },
|
||||
"moutai-base-wine-volume": { "key": "moutai-base-wine-volume", "title": "基酒产能、产量与销量", "csv": "data/moutai/moutai_base_wine_volume.csv", "height": 680, "filename": "贵州茅台基酒产能产量销量" },
|
||||
"moutai-flying-price": { "key": "moutai-flying-price", "title": "飞天茅台年度批价", "csv": "data/moutai/moutai_flying_price.csv", "height": 680, "filename": "飞天茅台年度批价" },
|
||||
"moutai-price-adjustment": { "key": "moutai-price-adjustment", "title": "飞天茅台提价记录", "csv": "data/moutai/moutai_price_adjustments.csv", "height": 680, "filename": "飞天茅台提价记录" },
|
||||
"moutai-ton-price": { "key": "moutai-ton-price", "title": "产品平均吨价", "csv": "data/moutai/moutai_ton_price.csv", "height": 680, "filename": "贵州茅台产品平均吨价" },
|
||||
"moutai-dividend": { "key": "moutai-dividend", "title": "分红与归母净利润", "csv": "data/moutai/moutai_dividend.csv", "height": 680, "filename": "贵州茅台分红与归母净利润" },
|
||||
"moutai-distributors": { "key": "moutai-distributors", "title": "经销商数量", "csv": "data/moutai/moutai_distributors.csv", "height": 680, "filename": "贵州茅台经销商数量" }
|
||||
},
|
||||
"tabs": [
|
||||
{ "id": "overview", "name": "概览", "chartRefs": ["moutai-profitability-annual", "moutai-profitability-quarterly", "moutai-product-annual", "moutai-channel-annual"] },
|
||||
{ "id": "profitability", "name": "盈利与现金流", "chartRefs": ["moutai-profitability-annual", "moutai-profitability-quarterly", "moutai-expense-annual", "moutai-expense-quarterly", "moutai-cashflow-annual", "moutai-cashflow-quarterly"] },
|
||||
{ "id": "balance", "name": "资产负债", "chartRefs": ["moutai-balance-annual", "moutai-balance-quarterly"] },
|
||||
{ "id": "classified-revenue", "name": "分类营收", "chartRefs": ["moutai-product-annual", "moutai-channel-annual", "moutai-classified-product-quarterly", "moutai-classified-channel-quarterly", "moutai-classified-product-cumulative", "moutai-classified-channel-cumulative", "moutai-revenue-composition-halfyear"] },
|
||||
{ "id": "operations", "name": "产能与渠道", "chartRefs": ["moutai-liquor-industry-output", "moutai-base-wine-volume", "moutai-ton-price", "moutai-distributors"] },
|
||||
{ "id": "price-shareholder", "name": "价格与股东回报", "chartRefs": ["moutai-flying-price", "moutai-price-adjustment", "moutai-dividend"] }
|
||||
]
|
||||
},
|
||||
{
|
||||
"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": "示例",
|
||||
|
||||
@@ -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.`);
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user