Compare commits
14 Commits
main
..
feat-valuer
| Author | SHA1 | Date | |
|---|---|---|---|
| 273177a89b | |||
| a46de853a0 | |||
| f646ee6718 | |||
| 3a3d19d5dd | |||
| a15b3f8027 | |||
| 417116b928 | |||
| a6387f8220 | |||
| 2a38778fa3 | |||
| bab02e1efd | |||
| 44c6437b64 | |||
| 9605228c3f | |||
| d093a322d3 | |||
| 04105ad6eb | |||
| 143cd1fbf8 |
@@ -29,6 +29,41 @@ pnpm test # 全量,只在改了 t
|
||||
|
||||
它会断言 `legend.top`、`grid.top`、折线 `symbolSize`/`borderWidth`,并检查 SVG 字体引号。回归测试直接遍历 `manifest.json` 的 `chartCatalog`,新增图只要完成 registry 与 manifest 登记就会被测到。
|
||||
|
||||
**带参数的图要另外跑参数测试。** 通用回归永远按默认值渲染,跑不到手输参数那条路径:
|
||||
|
||||
```bash
|
||||
pnpm test:param # 甜蜜击球区「现价」参数:标线位置 + 插值结果(两家公司 6 张图)
|
||||
```
|
||||
|
||||
**手输参数的图还要量几何、而且必须扫全价位。** 参数测试只查数值对不对,查不出「标签撞在一起」;而碰撞窗口常常只有几个价位宽 —— 甜蜜击球区实测:现价贴数据区下沿时顶部标注会撞左上角的区域带标签(宁德只有 220–240 会撞,抽查 310/400/700 全绿),现价落进关键价位旁一个数据格时两个数据标签会叠(宁德只在 265±20、腾讯只在 404.5±17):
|
||||
|
||||
```bash
|
||||
cp ~/.workbuddy/skills/smart-fin-chart-add-chart/scripts/geometry_check.mjs tmp_geometry.mjs
|
||||
node tmp_geometry.mjs --sweep sweet-tencent-price-ev sweet-catl-any-price # 全价位扫描
|
||||
node tmp_geometry.mjs --w=940 sweet-tencent-price-ev # 换画布宽度
|
||||
rm tmp_geometry.mjs
|
||||
```
|
||||
|
||||
## 图表参数(tab 级控件)
|
||||
|
||||
可手输的参数在 `manifest.json` 的 **tab** 上声明,不是在卡片上 —— 同一 tab 里的图共用一套口径(现价对三张击球区图是同一个事实),卡片级会变成三个各说各话的输入框:
|
||||
|
||||
```json
|
||||
"tabs": [{ "id": "catl", "name": "宁德时代",
|
||||
"controls": [{ "id": "price", "type": "number", "label": "现价", "unit": "元",
|
||||
"value": 316.21, "min": 220, "max": 700, "step": 1 }],
|
||||
"chartRefs": ["..."] }]
|
||||
```
|
||||
|
||||
- 链路:`renderTabControls`(ui.js,纯 DOM)→ `applyTabParam`(main.js,校验 + 防抖)→ `refreshChartParams`(复用实例 `setOption`,不 dispose 重建)→ 图表 builder 的第三个参数 `params`。
|
||||
- **位置**:`shell.params` 是页面里独立的一行,钉在 tab 栏正下方,与 tab 栏同处一个 sticky 容器 `.tab-header`(两行各自 sticky 就得给参数行算「tab 栏高度」的偏移,而 tab 栏会换行、窄屏还横向滚动,高度不固定)。没有 `controls` 的 tab 由 JS 置 `hidden`,整行含虚线分隔一并消失。改动这里要连带核对 `.card` 的 `scroll-margin-top`(桌面 128px / ≤880px 160px,= 页头底边 + 18px 呼吸)。
|
||||
- builder 签名写成 `(rows, { width, height, params })`,取值用 `resolvePrice(params)` 之类带兜底的解析函数:**没有 params 时必须回落到默认值**,否则 SSR 回归测试与脚本会拿到 NaN。
|
||||
- **可输区间 = 数据区,不是坐标轴**:手输值靠相邻行线性插值取值,落在数据区外只能得到 NaN。轴可以比数据区宽一点(留白),输入区间不行。
|
||||
- 手输值通常不是数据行,所以那些「只标关键价位」的 `labeledPrices` 要按数据行命中与否决定是否带上它,并做避让(见 `sweet_spot.js` 的 `refLabels`):现价与某个关键价位落在**同一个数据格**内时只留现价 —— 两个标签数值几乎相同、包围盒会实打实叠在一起。阈值取数据格步长(20)而不是现算像素,因为标签宽约 43px、每元约 1.8px,像素阈值会随轴范围漂。
|
||||
- **「现价」这类跟随输入变化的 graphic 标注要画在绘图区上方那条留白带里**(`top: LAYOUT.gridTop - 30`),不要贴绘图区顶边(`gridTop + 4`):贴边时它和绘图区顶行的文字抢位置,手输值落在低价端必然相撞(宁德 220 元时与「卖出区 >30x」重叠 35×8px)。画在外面只剩居中的图例在它上方,永不相交,顺带消除了「换个价、标注就上下跳」。
|
||||
- **纵轴下限要给「区域带名字」那一条留位置。** 纵向区域带的名字画在绘图区底边上方 12–25px,若数据线能压到接近下限,线标签就会和带名字重叠(宁德图 3 的「仅价差年化」在 700 元处跌到 -4.7%,轴下限 -10 只留 5.3pp 余量,手输 680 元时「-3.8%」压住「卖出区」22×7px)。改轴下限比挪标签便宜:图 3 的纵轴两家现在都是 -20 起。
|
||||
- 默认值的同源方要写在注释里。现价这套:`sweet_spot.js` 的 `profile.priceDefault`、manifest 的 `controls[].value`、`build_sweet_spot_catl.py` / `build_sweet_spot_tencent.py` 三处一起改。
|
||||
|
||||
**右侧纵向图例的图会报 `legend.top=middle (期望85)`,这是预期的,不要去"修"。** 那条断言的 85px 基准是给顶部横向图例定的,对 `orient: 'vertical'` + `top: 'middle'` 的图不适用(`ppmt_ip_share`、`ppmt_ip_share_history` 都会报)。
|
||||
|
||||
## 踩过的坑
|
||||
@@ -45,6 +80,10 @@ pnpm test # 全量,只在改了 t
|
||||
|
||||
## 数据口径
|
||||
|
||||
**底稿与图表数据分层。** `data/{company}/rawData/` 是唯一可维护的数据层。当前泡泡玛特的维护入口是其中的 `泡泡玛特_财报分析底稿.xlsx`:人工或 AI 从 PDF、截图、XLSX 提取的基础值先写入这份底稿,并保留来源文件、工作表和定位信息。`data/{company}/*.csv` 是图表运行时投影,只能由脚本生成,禁止手工编辑。
|
||||
|
||||
**维护明细,不维护可推导结果。** 收入、门店数、财报科目等基础值应保存;占比、同比、利润率、H2 单半年和分析分类小计应由脚本生成。若财报披露了占比,可作为核对值保留,但不是图表的唯一输入。资产负债表的“类现金/经营类资产/有息负债”等分类以独立规则映射明细科目,不复制成第二套人工数字。
|
||||
|
||||
**数值默认保留一位小数。** CSV 数据列与图表展示(柱/点标签、tooltip)统一保留一位小数(如 `71.1`、`311.4%`),四舍五入;确实需要其他精度时必须在 `footnote()` 或代码注释里说明原因。
|
||||
|
||||
**CSV 存四位年份**(`2026H1`),简写只在显示层做。核对数据时无歧义。
|
||||
@@ -57,6 +96,25 @@ pnpm test # 全量,只在改了 t
|
||||
|
||||
**桥图/瀑布图要验算闭合。** 各项占营收比是税前口径,加总到净利率前须乘税盾 `(1 - 有效税率)`;税率变化单独成项。恒等式 `净利率 = 税前利润率 × (1-税率)`,分解为 `Δ净 = Δ税前×(1-t₁) − 税前₂×Δt`。改完核对首尾误差应 < 0.05pct。
|
||||
|
||||
## 甜蜜击球区板块(股价 ↔ 估值)
|
||||
|
||||
菜单 id `sweet-spot`,两个 tab(`catl` 宁德时代 / `tencent` 腾讯控股),各三张图,**共用一套通用 builder**:
|
||||
|
||||
| | 宁德时代 | 腾讯控股 |
|
||||
| --- | --- | --- |
|
||||
| 纵轴口径 | `PE = 市值 ÷ 2026E 归母净利` | `EV/EBIT =(市值 − 非经营资产)÷ TTM EBIT` |
|
||||
| 扣什么 | 类现金 2,263.8 亿(纯现金,准现金) | 净现金 581.9 + 上市投资 4,872.0 + 非上市投资 387.9 = 5,841.8 亿(股权,会波动) |
|
||||
| 甜蜜区锚 | 13 / 15.5 / 25 / 30x(资料库原表五档) | 8 / 10 / 12x(巴菲特原始锚,白送 / 甜点 / 上沿) |
|
||||
| 数据区 / 输入区 | 220–700 元 | 240–640 港元 |
|
||||
| 默认现价 | 316.21 元(2026-09-15) | 426.8 港元(2026-09-17) |
|
||||
|
||||
**两家的口径差异不是随意选的**:宁德的类现金是现金,扣掉不改变分子的性质,PE 口径成立;腾讯的「类现金」主体是 5,260 亿投资组合(股权会波动),只扣净现金 581.9 亿几乎不改变倍数(少 0.23x),必须连投资组合一起扣才是经营资产的真实价格 —— 图 2 的三层线(不扣 / 只扣净现金 / 全扣)就是为把「漏扣投资组合会得出超上沿的假阴性」这件事画出来。
|
||||
|
||||
- 代码结构:`js/charts/sweet_spot.js` 里 `PROFILES = { catl, tencent }` 两个配置块 + 三个通用 builder(`buildMultipleChart` / `buildAssetTierChart` / `buildAnyPriceChart`),导出 6 个图表函数。**加第三家公司不要复制粘贴 builder**,照着配置块的字段补齐即可。
|
||||
- CSV 由 `scripts/build_sweet_spot_catl.py` / `build_sweet_spot_tencent.py` 生成,禁止手工编辑。改股本、汇率、利润预期 → 改脚本常量 → 重跑 → 图表自动跟着走(区域边界是从 CSV 反解的,没写死)。
|
||||
- 区域边界的**股价**必须从数据反解(`multToPrice`),缺席的阈值**留 `null` 占位不要 filter 掉**,否则后面档位整体前移一格(表现为「偏贵」色带铺到轴右端、最贵那档退化成零宽,且不报错)。
|
||||
- 验证:`node test/sweet_spot_param.mjs`(42 项,独立复算公式)+ `geometry_check.mjs --sweep`(全价位量几何)。
|
||||
|
||||
## 配色
|
||||
|
||||
**正式方向:轻快消费。** 柱线组合图的现金流、利润与同比使用 `FINANCIAL_COLORS`,渠道结构使用 `CHANNEL_COLORS`,瀑布图使用 `BRIDGE_COLORS`(蓝色基准、青绿色正向、珊瑚红负向);不要在图表文件里为相同财务/渠道语义重写十六进制颜色。柱状系列负责分类,折线只保留一条高辨识度强调色。
|
||||
@@ -91,6 +149,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。
|
||||
|
||||
@@ -179,3 +179,25 @@ A:编辑 `js/theme.js` 的 `PALETTE` 对象,所有图表自动同步。
|
||||
## 许可
|
||||
|
||||
本项目代码采用 MIT 协议,数据仅供示例。
|
||||
|
||||
## 估值工具
|
||||
|
||||
左侧选择「估值工具」,打开「目标收益反推增长率」Tab。支持反推所需归母利润年复合增长率、计算预期年化 IRR、反推最高买入价;可调整安全边际、股息、股本变化率、基期和币种汇率。参数自动保存在当前浏览器,点击「恢复示例参数」可重置。
|
||||
|
||||
入口:`#/valuation-tools/stock-return-valuation`。默认数字是教学情景假设,不是实时行情或财报。计算口径来自 `stock-return-valuation` 技能 v1.2.0,浏览器引擎位于 `js/tools/stock-return-math.js`,界面位于 `js/tools/stock-return-calculator.js` 和同名 HTML 模板。
|
||||
|
||||
单独运行估值检查:`npm run test:valuation`;`npm test` 同时运行图表回归与估值检查。
|
||||
|
||||
「DCF 估值」Tab(`#/valuation-tools/dcf`)采用独立的现金流工作台,可设置 1—5 个增长阶段,每段输入年数与现金流年增长率,总预测期不超过 30 年。支持 FCFF/WACC 与 FCFE/股权资本成本两种口径、永续增长终值、股权价值调整和逐年折现明细。
|
||||
|
||||
总股本在基础参数区单独填写,单位为亿股;每股价值 = 股权价值(亿元)÷ 当前总股本(亿股)。每家公司的股本、股本日期、现金流和增长假设独立保存在当前浏览器,也可新增公司。真实公司初次使用时,现金流与股本留空,需自行核实后填写;教学示例的 10 亿股仅为假设。当前模型使用人民币金额与预测期内不变的股本。
|
||||
|
||||
公司以按钮形式展示,点击即可切换;当前股价、股价日期和区间阈值也按公司自动保存到 LocalStorage,兼容原有 DCF 本地记录。股价需要手动输入,不自动请求行情。未填或填错股价不影响已完成的 DCF 计算,也不会保留上次股价标记。
|
||||
|
||||
ECharts 区间图以 DCF 每股价值为基准,默认低于其 80% 为低估、高于其 120% 为高估,两端边界均包含在合理区间内。股价旁直接展示低估折价(同时作为安全边际)与高估溢价阈值,可随时修改。安全边际价、低估价格及折价使用绿色,溢价与高估价格使用红色。这是当前假设下的自定义价格分区,不是独立的投资结论;无正股权价值时不显示区间。
|
||||
|
||||
首屏顶部并列展示 DCF 每股内在价值、安全边际参考价、当前股价及相对内在价值的折溢价。下方左侧编辑基础参数、分段增长率和股权调整,右侧查看现金流现值、终值与股权价值拆解。逐年现金流图表始终独立展示,计算明细可折叠展开;小屏幕自动改为上下排列。
|
||||
|
||||
DCF 页只计算现金流估值。预计 IRR、利润增长门槛和退出 PE 情景使用单独的「收益反推」Tab。现有公司的 DCF 记录沿用原有 LocalStorage 键,旧版额外字段不会参与 DCF 计算。
|
||||
|
||||
标题栏右侧的问号打开「估值概念说明」,包含 FCFF、FCFE、口径选择、折现率、永续终值、安全边际与区间说明,支持目录定位和键盘关闭。
|
||||
|
||||
+314
@@ -0,0 +1,314 @@
|
||||
/* DCF workbench uses the application's paper, grey and brown palette. */
|
||||
.dcf-calculator {
|
||||
--df-bg:var(--bg);
|
||||
--df-paper:var(--surface);
|
||||
--df-ink:var(--ink);
|
||||
--df-muted:#75716c;
|
||||
--df-line:var(--line);
|
||||
--df-accent:var(--accent);
|
||||
--df-soft:var(--accent-soft);
|
||||
--df-field:#faf9f7;
|
||||
--df-accent-line:#d7c7b6;
|
||||
--df-chart-secondary:#cbbba9;
|
||||
--df-error:#a63f32;
|
||||
--df-error-bg:#fff1ee;
|
||||
--df-low:#c4e6d1;
|
||||
--df-fair:#f1e0b3;
|
||||
--df-high:#efc7bd;
|
||||
--df-good:#287c54;
|
||||
--df-fair-ink:#786950;
|
||||
--df-high-ink:#b94c42;
|
||||
--df-warning-bg:#f6f2eb;
|
||||
display:grid;
|
||||
grid-template-columns:minmax(0,1fr);
|
||||
gap:20px;
|
||||
min-width:0;
|
||||
padding:26px;
|
||||
background:var(--df-bg);
|
||||
color:var(--df-ink);
|
||||
border:1px solid var(--df-line);
|
||||
border-radius:12px;
|
||||
font-size:13px;
|
||||
line-height:1.65;
|
||||
}
|
||||
.dcf-calculator * {box-sizing:border-box;}
|
||||
.dcf-calculator>* {min-width:0;}
|
||||
.dcf-calculator [hidden] {display:none!important;}
|
||||
.dcf-calculator :is(h2,h3,h4,p) {margin:0;}
|
||||
.dcf-calculator h2 {font-size:26px;font-weight:500;letter-spacing:-.7px;}
|
||||
.dcf-calculator h3 {font-size:14px;font-weight:500;}
|
||||
.dcf-calculator h4 {display:flex;align-items:center;gap:9px;font-size:12px;font-weight:500;}
|
||||
.dcf-calculator h4>span {font-size:11px;color:var(--df-accent);}
|
||||
.dcf-calculator b,.dcf-calculator strong {font-weight:500;font-variant-numeric:tabular-nums;}
|
||||
.dcf-calculator .df-muted {font-size:11px;color:var(--df-muted);line-height:1.7;}
|
||||
.dcf-calculator .df-header {display:flex;align-items:center;justify-content:space-between;gap:16px;}
|
||||
.dcf-calculator .df-eyebrow {font-size:11px;letter-spacing:1.5px;color:var(--df-accent);margin-bottom:4px;}
|
||||
.dcf-calculator .df-subtitle {margin-top:5px;font-size:12px;color:var(--df-muted);}
|
||||
.dcf-calculator .df-header-actions {display:flex;align-items:center;gap:12px;flex-shrink:0;}
|
||||
.dcf-calculator .df-header-mark {font-size:11px;white-space:nowrap;color:var(--df-muted);}
|
||||
.dcf-calculator button {
|
||||
font:inherit;
|
||||
font-size:11px;
|
||||
color:var(--df-accent);
|
||||
background:var(--df-paper);
|
||||
border:1px solid var(--df-line);
|
||||
border-radius:6px;
|
||||
padding:7px 11px;
|
||||
cursor:pointer;
|
||||
transition:background .15s,border-color .15s;
|
||||
}
|
||||
.dcf-calculator button:hover {background:var(--df-soft);border-color:var(--df-accent-line);}
|
||||
.dcf-calculator button:disabled {opacity:.4;cursor:default;}
|
||||
.dcf-calculator :is(input,select,button,summary):focus-visible {outline:2px solid var(--df-accent);outline-offset:3px;}
|
||||
.dcf-calculator .df-help-trigger {display:grid;place-items:center;width:34px;height:34px;padding:0;color:var(--df-muted);border-radius:50%;}
|
||||
.dcf-calculator .df-profile-bar {display:flex;flex-wrap:wrap;align-items:center;gap:10px;}
|
||||
.dcf-calculator .df-company-buttons {display:flex;flex-wrap:wrap;gap:10px;min-width:0;}
|
||||
.dcf-calculator .df-company-chip {position:relative;display:inline-flex;min-width:0;max-width:100%;}
|
||||
.dcf-calculator .df-company-buttons button {color:var(--df-muted);overflow-wrap:anywhere;max-width:100%;padding:7px 13px;}
|
||||
.dcf-calculator .df-company-buttons button[aria-pressed=true] {background:var(--df-soft);border-color:var(--df-accent-line);color:var(--df-accent);font-weight:500;}
|
||||
.dcf-calculator [data-new-company] {border-style:dashed;background:transparent;}
|
||||
.dcf-calculator .df-save-status {margin-left:auto;color:var(--df-muted);font-size:11px;}
|
||||
.dcf-calculator .df-new-company {display:flex;flex-wrap:wrap;align-items:end;gap:10px;}
|
||||
.dcf-calculator .df-new-company label {flex:1;min-width:160px;}
|
||||
.dcf-calculator .df-company-error {color:var(--df-error);font-size:12px;}
|
||||
.dcf-calculator label {display:grid;align-content:start;gap:6px;min-width:0;font-size:11px;color:var(--df-muted);}
|
||||
.dcf-calculator input,.dcf-calculator select {
|
||||
min-width:0;
|
||||
width:100%;
|
||||
height:39px;
|
||||
padding:8px 10px;
|
||||
font:inherit;
|
||||
font-size:13px;
|
||||
font-variant-numeric:tabular-nums;
|
||||
color:var(--df-ink);
|
||||
background:var(--df-field);
|
||||
border:1px solid var(--df-line);
|
||||
border-radius:5px;
|
||||
}
|
||||
.dcf-calculator input:focus,.dcf-calculator select:focus {border-color:var(--df-accent);background:var(--df-paper);}
|
||||
.dcf-calculator input::placeholder {font-size:11px;color:var(--muted);}
|
||||
.dcf-calculator details {padding:0;border:0;}
|
||||
.dcf-calculator summary {font-size:11px;color:var(--df-accent);cursor:pointer;}
|
||||
.dcf-calculator .df-overview {display:grid;grid-template-columns:1.2fr 1fr 1fr 1.2fr;background:var(--df-paper);border:1px solid var(--df-line);border-radius:10px;overflow:hidden;}
|
||||
.dcf-calculator .df-stat {display:flex;flex-direction:column;gap:8px;min-width:0;padding:20px 18px;}
|
||||
.dcf-calculator .df-stat+.df-stat {border-left:1px solid var(--df-line);}
|
||||
.dcf-calculator .df-stat h3 {font-size:11px;color:var(--df-muted);font-weight:400;}
|
||||
.dcf-calculator .df-stat-value {font-size:30px;font-weight:500;letter-spacing:-1px;line-height:1.4;font-variant-numeric:tabular-nums;overflow-wrap:anywhere;}
|
||||
.dcf-calculator .df-stat-main {background:var(--df-soft);}
|
||||
.dcf-calculator .df-stat-main .df-stat-value {color:var(--df-accent);}
|
||||
.dcf-calculator [data-buy],.dcf-calculator [data-zone-low],.dcf-calculator [data-deviation][data-price-relation=discount] {color:var(--df-good);}
|
||||
.dcf-calculator [data-zone-high],.dcf-calculator [data-deviation][data-price-relation=premium] {color:var(--df-high-ink);}
|
||||
.dcf-calculator .df-stat-position .df-stat-value {display:flex;align-items:center;min-height:42px;font-size:22px;letter-spacing:-.5px;}
|
||||
.dcf-calculator :is(.df-market,.df-form,.df-results,.df-cashflow-section) {min-width:0;padding:20px;background:var(--df-paper);border:1px solid var(--df-line);border-radius:10px;}
|
||||
.dcf-calculator .df-section-head,.dcf-calculator .df-market-head {display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:10px;}
|
||||
.dcf-calculator .df-section-head {margin-bottom:16px;}
|
||||
.dcf-calculator .df-market-head p {margin-top:3px;}
|
||||
.dcf-calculator .df-zone-badge {padding:3px 8px;font-size:11px;white-space:nowrap;border-radius:4px;}
|
||||
.dcf-calculator .df-zone-badge[data-zone=低估] {background:var(--df-low);color:var(--df-good);}
|
||||
.dcf-calculator .df-zone-badge[data-zone=合理] {background:var(--df-fair);color:var(--df-fair-ink);}
|
||||
.dcf-calculator .df-zone-badge[data-zone=高估] {background:var(--df-high);color:var(--df-high-ink);}
|
||||
.dcf-calculator .df-price-controls {display:grid;grid-template-columns:repeat(2,minmax(0,1fr));align-items:end;gap:16px;margin-top:14px;}
|
||||
.dcf-calculator .df-price-fields {display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;min-width:0;}
|
||||
.dcf-calculator .df-interval-fields {min-width:0;}
|
||||
.dcf-calculator .df-price-chart {width:100%;min-width:0;height:180px;overflow:hidden;}
|
||||
.dcf-calculator .df-zone-keys {display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:12px;}
|
||||
.dcf-calculator .df-zone-key {display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:5px;padding:8px 10px;background:var(--df-field);border-radius:5px;font-size:11px;}
|
||||
.dcf-calculator .df-zone-key strong {font-size:11px;}
|
||||
.dcf-calculator .df-zone-dot {display:inline-block;width:5px;height:5px;margin-right:6px;border-radius:50%;background:var(--df-good);}
|
||||
.dcf-calculator .df-zone-key:nth-child(2) .df-zone-dot {background:var(--df-fair-ink);}
|
||||
.dcf-calculator .df-zone-key:nth-child(3) .df-zone-dot {background:var(--df-high-ink);}
|
||||
.dcf-calculator .df-market-message,.dcf-calculator .df-interval-note {margin-top:12px;}
|
||||
.dcf-calculator .df-market .df-error {margin-top:12px;}
|
||||
.dcf-calculator .df-layout {display:grid;grid-template-columns:minmax(0,1.35fr) minmax(0,1fr);align-items:start;gap:20px;}
|
||||
.dcf-calculator .df-parameter-group {border-top:1px solid var(--df-line);padding-top:18px;margin-top:18px;}
|
||||
.dcf-calculator .df-parameter-group .df-section-head {margin-bottom:12px;}
|
||||
.dcf-calculator .df-fields {display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;}
|
||||
.dcf-calculator .df-parameter-group>h4 {margin-bottom:12px;}
|
||||
.dcf-calculator .df-parameter-group>.df-muted {margin-top:10px;}
|
||||
.dcf-calculator .df-quiet-button {border:0;background:transparent;padding:2px 0;}
|
||||
.dcf-calculator .df-stages {display:grid;gap:8px;}
|
||||
.dcf-calculator .df-stage-row {display:grid;grid-template-columns:minmax(55px,1fr) 65px minmax(70px,1fr) 22px;align-items:center;gap:8px;padding:10px;background:var(--df-field);border:1px solid var(--df-line);border-radius:6px;}
|
||||
.dcf-calculator .df-stage-row input {height:34px;padding:6px;background:var(--df-paper);}
|
||||
.dcf-calculator .df-stage-row button {padding:2px;border:0;background:transparent;color:var(--df-muted);font-size:17px;}
|
||||
.dcf-calculator .df-stage-name {min-width:0;font-size:12px;}
|
||||
.dcf-calculator .df-stage-name small {display:block;font-size:11px;color:var(--df-muted);}
|
||||
.dcf-calculator .df-stage-key {font-size:11px;color:var(--df-muted);}
|
||||
.dcf-calculator .df-total {margin-top:10px;}
|
||||
.dcf-calculator .df-result-line {display:flex;align-items:baseline;justify-content:space-between;gap:12px;padding:9px 0;font-size:12px;}
|
||||
.dcf-calculator .df-result-line>* {min-width:0;overflow-wrap:anywhere;}
|
||||
.dcf-calculator .df-result-line b {text-align:right;}
|
||||
.dcf-calculator .df-sum {border-top:1px solid var(--df-line);margin:6px 0;}
|
||||
.dcf-calculator .df-equity {border-top:1px solid var(--df-line);margin-top:7px;padding-top:13px;}
|
||||
.dcf-calculator .df-equity b {font-size:22px;}
|
||||
.dcf-calculator .df-shares-note {text-align:right;}
|
||||
.dcf-calculator .df-value-per-share {display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:8px;padding:12px;margin-top:14px;background:var(--df-soft);border-radius:6px;font-size:12px;}
|
||||
.dcf-calculator .df-value-per-share strong {font-size:24px;color:var(--df-accent);overflow-wrap:anywhere;}
|
||||
.dcf-calculator .df-value-per-share small {font-size:11px;}
|
||||
.dcf-calculator .df-composition {display:flex;gap:2px;height:7px;margin:23px 0 8px;border-radius:4px;overflow:hidden;background:var(--df-line);}
|
||||
.dcf-calculator .df-segment-a {background:var(--df-accent);}
|
||||
.dcf-calculator .df-segment-b {background:var(--df-chart-secondary);}
|
||||
.dcf-calculator .df-legend {display:flex;justify-content:space-between;flex-wrap:wrap;gap:8px;font-size:11px;color:var(--df-muted);}
|
||||
.dcf-calculator .df-terminal-note {padding:12px;margin-top:18px;background:var(--df-field);border-radius:6px;font-size:11px;color:var(--df-muted);}
|
||||
.dcf-calculator .df-cashflow-chart {height:300px;width:100%;min-width:0;}
|
||||
.dcf-calculator .df-cashflow-section h3 {margin-bottom:4px;}
|
||||
.dcf-calculator .df-table-section {margin-top:16px;padding-top:14px;border-top:1px solid var(--df-line);}
|
||||
.dcf-calculator .df-table-wrap {max-width:100%;margin-top:12px;overflow-x:auto;}
|
||||
.dcf-calculator table {border-collapse:collapse;white-space:nowrap;font-variant-numeric:tabular-nums;width:100%;font-size:11px;}
|
||||
.dcf-calculator th,.dcf-calculator td {text-align:right;border-bottom:1px solid var(--df-line);padding:10px 8px;}
|
||||
.dcf-calculator th {color:var(--df-muted);background:var(--df-field);font-weight:400;}
|
||||
.dcf-calculator th:first-child,.dcf-calculator td:first-child {text-align:left;}
|
||||
.dcf-calculator .df-foot {display:flex;justify-content:space-between;flex-wrap:wrap;gap:8px;color:var(--df-muted);font-size:11px;}
|
||||
.dcf-calculator .df-error {color:var(--df-error);background:var(--df-error-bg);border:1px solid var(--df-high);border-radius:6px;padding:12px 14px;font-size:12px;}
|
||||
.dcf-calculator .df-warning {color:var(--df-fair-ink);background:var(--df-warning-bg);border-radius:6px;padding:12px 14px;font-size:12px;}
|
||||
/* Keep the existing three-second dwell behavior for company deletion. */
|
||||
.dcf-calculator .df-company-buttons .df-company-delete {position:absolute;top:-7px;right:-7px;display:grid;place-items:center;width:20px;height:20px;padding:0;border:1px solid var(--df-accent-line);border-radius:50%;background:var(--df-paper);color:var(--df-muted);font-size:16px;line-height:1;z-index:1;opacity:0;visibility:hidden;transition:opacity .15s,visibility 0s;}
|
||||
.dcf-calculator .df-company-chip:is(:hover,:focus-within) .df-company-delete {opacity:1;visibility:visible;transition:opacity .15s 3s,visibility 0s 3s;}
|
||||
.dcf-calculator .df-company-buttons .df-company-delete:hover {color:var(--df-error);border-color:var(--df-error);background:var(--df-error-bg);}
|
||||
.dcf-calculator .df-company-empty {padding:28px 20px;border:1px dashed var(--df-line);border-radius:10px;color:var(--df-muted);text-align:center;}
|
||||
.dcf-calculator[data-empty=true] :is(.df-overview,.df-market,.df-layout,.df-cashflow-section,.df-foot,.df-error,.df-warning) {display:none!important;}
|
||||
@media (width>=1500px) {.tools-layout .content:has(.dcf-calculator) {max-width:1280px;}}
|
||||
@media (width<=1100px) {
|
||||
.dcf-calculator {padding:20px;gap:18px;}
|
||||
.dcf-calculator .df-stat {padding:16px 12px;}
|
||||
.dcf-calculator .df-stat-value {font-size:27px;}
|
||||
.dcf-calculator .df-stat-position .df-stat-value {font-size:20px;min-height:38px;}
|
||||
.dcf-calculator .df-layout {gap:16px;}
|
||||
.dcf-calculator :is(.df-form,.df-results) {padding:16px;}
|
||||
.dcf-calculator .df-stage-row {grid-template-columns:minmax(48px,1fr) 52px minmax(65px,1fr) 20px;gap:6px;padding:8px;}
|
||||
.dcf-calculator .df-save-status {width:100%;margin-left:0;}
|
||||
}
|
||||
@media (width<=950px) {
|
||||
.dcf-calculator .df-layout {grid-template-columns:1fr;}
|
||||
.dcf-calculator .df-overview {grid-template-columns:repeat(2,minmax(0,1fr));}
|
||||
.dcf-calculator .df-stat:nth-child(3) {border-left:0;border-top:1px solid var(--df-line);}
|
||||
.dcf-calculator .df-stat:nth-child(4) {border-top:1px solid var(--df-line);}
|
||||
}
|
||||
@media (width<=680px) {
|
||||
.dcf-calculator {padding:16px;gap:16px;border-radius:10px;}
|
||||
.dcf-calculator h2 {font-size:23px;}
|
||||
.dcf-calculator .df-header-mark {display:none;}
|
||||
.dcf-calculator .df-header-actions {gap:6px;}
|
||||
.dcf-calculator .df-market {padding:16px;}
|
||||
.dcf-calculator .df-price-controls {grid-template-columns:minmax(0,1fr);gap:12px;}
|
||||
.dcf-calculator .df-zone-keys {gap:8px;}
|
||||
.dcf-calculator .df-zone-key {display:grid;padding:8px;gap:4px;}
|
||||
.dcf-calculator .df-cashflow-section {padding:16px;}
|
||||
.dcf-calculator .df-cashflow-chart {height:260px;}
|
||||
}
|
||||
@media (width<=460px) {
|
||||
.dcf-calculator {padding:12px;}
|
||||
.dcf-calculator input,.dcf-calculator select {font-size:16px;}
|
||||
.dcf-calculator .df-fields {gap:10px;}
|
||||
.dcf-calculator .df-stage-row {grid-template-columns:minmax(42px,1fr) 48px minmax(56px,1fr) 20px;gap:5px;padding:7px;}
|
||||
.dcf-calculator .df-stage-name {font-size:11px;}
|
||||
.dcf-calculator .df-stage-name small {font-size:11px;}
|
||||
.dcf-calculator .df-stat-value {font-size:25px;}
|
||||
.dcf-calculator .df-stat-position .df-stat-value {font-size:20px;min-height:35px;}
|
||||
}
|
||||
@media (hover:none) {.dcf-calculator .df-company-buttons .df-company-delete {opacity:1;visibility:visible;transition:none;}}
|
||||
@media (pointer:coarse) {
|
||||
.dcf-calculator input,.dcf-calculator select {min-height:44px;height:auto;font-size:16px;}
|
||||
.dcf-calculator button:not(.df-company-delete) {min-height:44px;}
|
||||
.dcf-calculator .df-help-trigger {width:44px;}
|
||||
}
|
||||
@media (prefers-reduced-motion:reduce) {.dcf-calculator * {transition:none!important;}}
|
||||
@media (prefers-reduced-motion:reduce) and (hover:hover) {.dcf-calculator .df-company-chip:is(:hover,:focus-within) .df-company-delete {transition:visibility 0s 3s!important;}}
|
||||
|
||||
/* Concept documentation dialog. */
|
||||
.dcf-calculator .df-help-dialog {
|
||||
width:min(860px,calc(100% - 32px));
|
||||
max-width:none;
|
||||
max-height:calc(100vh - 32px);
|
||||
max-height:calc(100dvh - 32px);
|
||||
margin:auto;
|
||||
padding:0;
|
||||
border:1px solid var(--df-line);
|
||||
border-radius:12px;
|
||||
background:var(--df-paper);
|
||||
color:var(--df-ink);
|
||||
box-shadow:0 18px 60px rgb(51 51 51 / 15%);
|
||||
overflow:hidden}
|
||||
.dcf-calculator .df-help-dialog[open] {
|
||||
display:flex;
|
||||
flex-direction:column}
|
||||
.dcf-calculator .df-help-dialog::backdrop {
|
||||
background:rgb(51 51 51 / 35%)}
|
||||
.dcf-calculator .df-help-head {
|
||||
display:flex;
|
||||
align-items:center;
|
||||
justify-content:space-between;
|
||||
gap:16px;
|
||||
padding:20px 24px 16px;
|
||||
border-bottom:1px solid var(--df-line)}
|
||||
.dcf-calculator .df-help-head h2 {
|
||||
margin:0 0 4px;
|
||||
font-size:20px;
|
||||
letter-spacing:0}
|
||||
.dcf-calculator .df-help-close {
|
||||
flex-shrink:0;
|
||||
width:32px;
|
||||
height:32px;
|
||||
padding:0;
|
||||
font-size:22px;
|
||||
line-height:1;
|
||||
color:var(--df-muted)}
|
||||
.dcf-calculator .df-help-nav {
|
||||
display:flex;
|
||||
flex-shrink:0;
|
||||
flex-wrap:wrap;
|
||||
gap:8px;
|
||||
padding:12px 24px;
|
||||
border-bottom:1px solid var(--df-line);
|
||||
background:var(--df-field)}
|
||||
.dcf-calculator .df-help-body {
|
||||
min-height:0;
|
||||
overflow-y:auto;
|
||||
overscroll-behavior:contain;
|
||||
padding:20px 24px 24px;
|
||||
font-size:13px;
|
||||
line-height:1.85}
|
||||
.dcf-calculator .df-help-lead {
|
||||
padding:14px 16px;
|
||||
background:var(--df-soft);
|
||||
border-radius:7px}
|
||||
.dcf-calculator .df-help-body section {
|
||||
padding-top:24px;
|
||||
outline-offset:-2px}
|
||||
.dcf-calculator .df-help-body h3 {
|
||||
margin-bottom:10px;
|
||||
color:var(--df-accent);
|
||||
font-size:15px}
|
||||
.dcf-calculator .df-help-body section p+p {
|
||||
margin-top:10px}
|
||||
.dcf-calculator .df-help-formula {
|
||||
margin:12px 0;
|
||||
padding:12px 14px;
|
||||
border-left:3px solid var(--df-accent-line);
|
||||
background:var(--df-field);
|
||||
font-variant-numeric:tabular-nums;
|
||||
overflow-wrap:anywhere}
|
||||
.dcf-calculator .df-help-table {
|
||||
margin:12px 0;
|
||||
overflow-x:auto}
|
||||
.dcf-calculator .df-help-table table {
|
||||
font-size:12px;
|
||||
white-space:normal}
|
||||
.dcf-calculator .df-help-table :is(th,td) {
|
||||
padding:10px;
|
||||
text-align:left}
|
||||
.dcf-calculator .df-help-source {
|
||||
margin-top:24px;
|
||||
padding-top:14px;
|
||||
border-top:1px solid var(--df-line);
|
||||
font-size:11px;
|
||||
color:var(--df-muted)}
|
||||
.dcf-calculator .df-help-source a {
|
||||
color:var(--df-accent);
|
||||
text-underline-offset:3px}
|
||||
@media (width<=680px) {
|
||||
.dcf-calculator .df-header-actions {gap:6px}
|
||||
.dcf-calculator .df-help-head {padding:16px}
|
||||
.dcf-calculator .df-help-nav {padding:12px 16px;gap:6px}
|
||||
.dcf-calculator .df-help-body {padding:16px}
|
||||
.dcf-calculator .df-help-table :is(th,td) {padding:8px 6px}
|
||||
}
|
||||
+363
-19
@@ -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;
|
||||
@@ -50,13 +53,211 @@ button {
|
||||
background: var(--accent);
|
||||
border-color: var(--accent);
|
||||
border-radius: 50%;
|
||||
font-size: 22px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.reload-button:hover { opacity: .88; transform: rotate(-45deg); }
|
||||
/* 图标继承按钮的 color(currentColor),尺寸只在这里定义,换图标不用动 JS。
|
||||
20px 是配 36px 圆钮的光学最优值:新图标的弧线比原 ↻ 字形细,19px 会显轻。 */
|
||||
.reload-button .reload-icon {
|
||||
display: block;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
fill: currentColor;
|
||||
}
|
||||
|
||||
/* 悬停转一圈:刷新语义的标准反馈,比原先 ↻ 字形的 -45° 倾斜更贴合新图标 */
|
||||
.reload-button:hover { opacity: .88; }
|
||||
.reload-button:hover .reload-icon { animation: reload-spin .6s ease-in-out; }
|
||||
|
||||
@keyframes reload-spin {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.reload-button:hover .reload-icon { animation: none; }
|
||||
}
|
||||
.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;
|
||||
@@ -122,30 +323,109 @@ button {
|
||||
|
||||
.content { min-width: 0; }
|
||||
|
||||
.tabs {
|
||||
/* ── 页头:tab 栏 + 参数行 ─────────────────────────
|
||||
sticky 只挂在容器上,里面两行一起钉住。底色比过去(.6)更实、并加一层模糊:
|
||||
现在压在卡片上方的是两行内容,透过去的字会干扰读数。 */
|
||||
.tab-header {
|
||||
position: sticky;
|
||||
top: 20px;
|
||||
z-index: 5;
|
||||
margin-bottom: 18px;
|
||||
padding: 5px;
|
||||
background: rgba(255, 255, 255, .82);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 9px;
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.tabs {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
margin-bottom: 18px;
|
||||
padding: 5px;
|
||||
background: rgba(255, 255, 255, .6);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 9px;
|
||||
}
|
||||
|
||||
.tab-button { padding: 7px 11px; }
|
||||
|
||||
/* ── tab 级参数控件(现价) ─────────────────────────
|
||||
独立一行,贴在 tab 栏下沿;一条虚线分隔说明「这不是导航,是改口径的输入」。
|
||||
没有参数的 tab 会被 JS 置 hidden,此时整行(含分隔线)不占位。 */
|
||||
|
||||
.tab-controls {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-top: 5px;
|
||||
padding: 6px 3px 1px;
|
||||
border-top: 1px dashed var(--line);
|
||||
}
|
||||
|
||||
/* hidden 属性会被上面的 display:flex 盖掉,显式收回来 */
|
||||
.tab-controls[hidden] { display: none; }
|
||||
|
||||
.param-field {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 5px;
|
||||
padding: 4px 8px;
|
||||
background: #f4f2ee;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 6px;
|
||||
transition: border-color .15s, background .15s;
|
||||
}
|
||||
.param-field:hover { border-color: var(--line); }
|
||||
.param-field:focus-within { background: var(--surface); border-color: var(--accent); }
|
||||
|
||||
.param-label { color: var(--muted); font-size: 12px; white-space: nowrap; }
|
||||
|
||||
/* 输入框只留一条下划线:数字是这里的主角,不要给它加边框抢视线 */
|
||||
.param-input {
|
||||
width: 94px;
|
||||
padding: 1px 2px;
|
||||
border: 0;
|
||||
border-bottom: 1px solid var(--line);
|
||||
background: transparent;
|
||||
color: var(--ink);
|
||||
font: inherit;
|
||||
font-size: 13px;
|
||||
font-variant-numeric: tabular-nums;
|
||||
font-weight: 600;
|
||||
text-align: right;
|
||||
}
|
||||
.param-input:focus { outline: none; border-bottom-color: var(--accent); }
|
||||
.param-input[data-invalid="true"] { color: #a63f32; border-bottom-color: #a63f32; }
|
||||
/* 上下箭头在 Chrome 里只在悬停/聚焦时出现,保留(步进 1 元比手敲快) */
|
||||
.param-input::-webkit-inner-spin-button { opacity: .45; }
|
||||
|
||||
.param-unit { color: var(--muted); font-size: 12px; }
|
||||
|
||||
/* 合法区间常驻:输入越界时它变红,不用猜上下限 */
|
||||
.param-range { color: var(--muted); font-size: 11px; white-space: nowrap; }
|
||||
.param-input[data-invalid="true"] ~ .param-range { color: #a63f32; }
|
||||
|
||||
.param-reset {
|
||||
padding: 5px 8px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 6px;
|
||||
background: transparent;
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.param-reset:hover:not(:disabled) { color: var(--accent); border-color: var(--line); }
|
||||
.param-reset:disabled { opacity: .35; cursor: default; }
|
||||
|
||||
.cards { display: grid; gap: 28px; }
|
||||
|
||||
/* scroll-margin-top = 页头底边(sticky top 20 + 页头实测高 89)+ 18px 呼吸:
|
||||
页头是 tab 栏 + 参数行两行,比过去的单行 tab 栏高约 43px,锚点跳转时标题才不会被盖住。
|
||||
页头高度会随 tab 换行浮动,这里按单行 tab 的最常见形态取值。 */
|
||||
.card {
|
||||
overflow: hidden;
|
||||
background: var(--surface);
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, .06);
|
||||
scroll-margin-top: 84px;
|
||||
scroll-margin-top: 128px;
|
||||
}
|
||||
|
||||
.card-bar {
|
||||
@@ -216,26 +496,90 @@ button {
|
||||
}
|
||||
.company-sidebar .sidebar-title { display: none; }
|
||||
.company-list { display: flex; overflow-x: auto; }
|
||||
/* 隐藏横向滚动条(与 .tabs 同处理):classic 滚动条会占掉 15px 高度,
|
||||
把贴在公司栏下方的页头压住 —— 而页头的 sticky top 是写死的 50px。 */
|
||||
.company-list { scrollbar-width: none; }
|
||||
.company-list::-webkit-scrollbar { display: none; }
|
||||
.company-button { flex: 0 0 auto; width: auto; }
|
||||
.tabs {
|
||||
.tab-header {
|
||||
top: 50px;
|
||||
z-index: 6;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
flex-wrap: nowrap;
|
||||
margin: 0 -12px 18px;
|
||||
padding: 5px 12px;
|
||||
padding: 5px 12px 7px;
|
||||
border-width: 0 0 1px;
|
||||
border-radius: 0;
|
||||
background: rgba(245, 244, 241, .96);
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
/* tab 按钮多时横向滚动 —— sticky 容器不能自己滚(会把参数行一起带走),
|
||||
所以滚动只加在 .tabs 上,并用负 margin 抵消容器横向 padding 让滚动区撑到屏幕边 */
|
||||
.tabs {
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
flex-wrap: nowrap;
|
||||
margin: 0 -12px;
|
||||
padding: 0 12px;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
.tabs::-webkit-scrollbar { display: none; }
|
||||
.tab-button { flex: 0 0 auto; }
|
||||
.card { scroll-margin-top: 120px; }
|
||||
/* 参数行自成一行,不再跟 tab 栏抢横向空间:窄屏直接换行,分隔线通到屏幕两边 */
|
||||
.tab-controls { margin: 5px -12px 0; padding: 7px 12px 1px; }
|
||||
/* 公司栏 50 + 页头 91(tab 栏 33 + 参数行 41 及间距)+ 18px 呼吸 */
|
||||
.card { scroll-margin-top: 160px; }
|
||||
.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; }
|
||||
}
|
||||
|
||||
/* 估值工具沿用图表页的纸白与棕色主题。 */
|
||||
.tools-title { margin-top: 14px; padding-top: 14px; border-top: 1px solid var(--line); }
|
||||
.app-layout.tools-layout { grid-template-columns: 184px minmax(0, 1fr); }
|
||||
.tools-layout .content { max-width: 1120px; width: 100%; }
|
||||
.chart-sidebar[hidden] { display: none; }
|
||||
.return-calculator {
|
||||
display: grid;
|
||||
gap: 22px;
|
||||
min-width: 0;
|
||||
padding: 26px;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 10px;
|
||||
}
|
||||
.return-calculator [hidden] { display: none !important; }
|
||||
.return-calculator h2 { margin: 0 0 10px; font-size: 22px; font-weight: 600; }
|
||||
.return-calculator [data-company] { margin-bottom: 6px; font-size: 13px; color: var(--accent); }
|
||||
.return-calculator .text-small { font-size: 12px; line-height: 1.8; }
|
||||
.return-calculator .text-muted { color: #75716c; }
|
||||
.return-calculator .viz-controls { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 18px 16px; }
|
||||
.return-calculator .form-label { display: grid; align-content: start; gap: 8px; min-width: 0; font-size: 12px; line-height: 1.6; }
|
||||
.return-calculator .form-control,
|
||||
.return-calculator .form-select {
|
||||
width: 100%; min-width: 0; padding: 10px 12px; border: 1px solid var(--line);
|
||||
border-radius: 6px; background: #faf9f7; color: var(--ink); font: inherit;
|
||||
font-size: 15px; font-variant-numeric: tabular-nums;
|
||||
}
|
||||
.return-calculator .form-control:focus,
|
||||
.return-calculator .form-select:focus { outline: 2px solid var(--accent-soft); border-color: var(--accent); }
|
||||
.return-calculator details { padding: 14px 0; border-top: 1px solid var(--line); border-bottom: 1px solid var(--line); }
|
||||
.return-calculator summary { cursor: pointer; font-size: 13px; color: var(--accent); }
|
||||
.return-calculator .calc-advanced { padding-top: 20px; }
|
||||
.return-calculator .calc-actions { display: flex; align-items: center; flex-wrap: wrap; gap: 12px; }
|
||||
.return-calculator .calc-message { margin: 0; }
|
||||
.return-calculator .text-destructive { padding: 12px; border-radius: 6px; color: #a63f32; background: #fff1ee; font-size: 13px; }
|
||||
.return-calculator .calc-section { display: grid; gap: 12px; min-width: 0; }
|
||||
.return-calculator .viz-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px; }
|
||||
.return-calculator .viz-stat { padding: 18px 14px; border: 1px solid var(--line); box-shadow: none; background: #faf9f7; font-size: 12px; overflow-wrap: anywhere; }
|
||||
.return-calculator .viz-stat:first-child { background: var(--accent-soft); border-color: var(--accent-soft); }
|
||||
.return-calculator .viz-stat-value { margin-top: 12px; font-size: clamp(18px, 1.7vw, 26px); font-weight: 600; color: var(--accent); font-variant-numeric: tabular-nums; }
|
||||
.return-calculator .calc-notes { padding-top: 8px; border-top: 1px solid var(--line); }
|
||||
.return-calculator .calc-notes p { margin: 8px 0 0; }
|
||||
@media (max-width: 680px) {
|
||||
.return-calculator { padding: 20px 16px; gap: 20px; }
|
||||
.return-calculator h2 { font-size: 20px; }
|
||||
.return-calculator .viz-controls { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 16px 12px; }
|
||||
.return-calculator .viz-grid { grid-template-columns: 1fr; }
|
||||
.return-calculator .viz-stat-value { font-size: 24px; }
|
||||
}
|
||||
|
||||
@@ -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,7 @@
|
||||
company,gp_per_wh,margin_data
|
||||
宁德时代,0.122,20.63
|
||||
比亚迪*,0.097,21.00
|
||||
LG新能源†,-0.010,-0.70
|
||||
中创新航*,0.077,16.71
|
||||
国轩高科*,0.063,14.02
|
||||
亿纬锂能,0.078,16.19
|
||||
|
@@ -0,0 +1,7 @@
|
||||
period,catl,byd,calb,guoxuan,eve
|
||||
2021,0.784,0.720,0.660,0.683,
|
||||
2022,0.978,0.800,0.872,0.840,
|
||||
2023,0.889,0.700,0.618,0.720,
|
||||
2024,0.664,0.550,0.496,0.626,
|
||||
2025,0.585,0.470,0.459,0.473,0.516
|
||||
2026H1,0.591,0.460,0.459,0.452,0.483
|
||||
|
@@ -0,0 +1,7 @@
|
||||
company,gp_2023,gp_2026h1
|
||||
宁德时代,0.198,0.122
|
||||
比亚迪*,0.154,0.097
|
||||
LG新能源†,0.119,-0.010
|
||||
中创新航*,0.080,0.077
|
||||
国轩高科*,0.112,0.063
|
||||
亿纬锂能,,0.078
|
||||
|
@@ -0,0 +1,7 @@
|
||||
period,catl,peer_avg,gap_multiple
|
||||
2021,0.172,0.080,2.15
|
||||
2022,0.168,0.118,1.42
|
||||
2023,0.198,0.096,2.06
|
||||
2024,0.159,0.087,1.83
|
||||
2025,0.139,0.075,1.85
|
||||
2026H1,0.122,0.073,1.68
|
||||
|
@@ -0,0 +1,7 @@
|
||||
period,catl,byd,lges,calb,guoxuan,eve
|
||||
2021,0.172,0.144,0.067,0.037,0.123,
|
||||
2022,0.168,0.144,0.091,0.090,0.147,
|
||||
2023,0.198,0.154,0.119,0.080,0.112,
|
||||
2024,0.159,0.121,0.032,0.079,0.095,
|
||||
2025,0.139,0.103,0.068,0.077,0.068,0.080
|
||||
2026H1,0.122,0.097,-0.010,0.077,0.063,0.078
|
||||
|
@@ -0,0 +1,7 @@
|
||||
period,catl,byd,lges,calb,guoxuan,global_total
|
||||
2021,99.5,26.4,59.4,8.0,6.7,301.5
|
||||
2022,191.6,70.4,70.4,20.0,14.1,517.9
|
||||
2023,259.7,111.4,95.8,33.4,17.1,705.5
|
||||
2024,339.3,153.7,96.3,39.4,28.5,894.4
|
||||
2025,464.7,194.8,108.8,62.8,53.5,1187.0
|
||||
2026H1,242.7,87.7,52.6,31.2,28.0,608.5
|
||||
|
@@ -0,0 +1,7 @@
|
||||
period,catl,byd,lges,calb,guoxuan
|
||||
2021,33.0,8.7,19.7,2.6,2.2
|
||||
2022,37.0,13.6,13.6,3.9,2.7
|
||||
2023,36.8,15.8,13.6,4.7,2.4
|
||||
2024,37.9,17.2,10.8,4.4,3.2
|
||||
2025,39.2,16.4,9.2,5.3,4.5
|
||||
2026H1,39.9,14.4,8.6,5.1,4.6
|
||||
|
@@ -0,0 +1,7 @@
|
||||
period,catl,byd,lges,calb,guoxuan,eve
|
||||
2021,22.00,20.00,4.31,5.60,18.00,
|
||||
2022,17.17,18.00,4.74,10.32,17.50,
|
||||
2023,22.27,22.00,6.41,13.00,15.50,
|
||||
2024,23.94,22.00,2.25,15.89,15.14,
|
||||
2025,23.84,22.00,5.69,16.69,14.39,15.50
|
||||
2026H1,20.63,21.00,-0.70,16.71,14.02,16.19
|
||||
|
@@ -0,0 +1,7 @@
|
||||
period,catl,byd,lges,calb,guoxuan
|
||||
2021,95.1,,73.4,95.2,41.0
|
||||
2022,83.3,71.0,73.6,93.8,46.0
|
||||
2023,70.5,72.0,69.3,75.6,48.0
|
||||
2024,76.3,70.0,57.8,78.7,46.0
|
||||
2025,96.9,82.0,47.6,,71.0
|
||||
2026H1,94.9,,52.8,,
|
||||
|
@@ -0,0 +1,7 @@
|
||||
period,sales_gwh,revenue_yi,asp,cost_per_wh,gp_per_wh,gross_margin
|
||||
2021,116.7,914.9,0.784,0.612,0.172,22.00
|
||||
2022,242.0,2365.9,0.978,0.810,0.168,17.17
|
||||
2023,321.0,2852.5,0.889,0.691,0.198,22.27
|
||||
2024,381.0,2530.4,0.664,0.505,0.159,23.94
|
||||
2025,541.0,3165.1,0.585,0.446,0.139,23.84
|
||||
2026H1,325,1921.2,0.591,0.469,0.122,20.63
|
||||
|
@@ -0,0 +1,7 @@
|
||||
period,china,korea,japan,others,global_total
|
||||
2021,48.2,30.2,12.0,9.6,301.5
|
||||
2022,60.4,23.7,7.3,8.6,517.9
|
||||
2023,63.5,23.2,6.1,7.2,705.5
|
||||
2024,67.1,18.5,3.9,10.5,894.4
|
||||
2025,70.4,15.3,3.7,10.6,1187.0
|
||||
2026H1,72.4,11.7,3.7,12.2,608.5
|
||||
|
@@ -0,0 +1,11 @@
|
||||
rank,company,region,gwh,share,yoy,global_total
|
||||
1,宁德时代,中国,242.7,39.9,25.3,608.5
|
||||
2,比亚迪,中国,87.7,14.4,1.6,608.5
|
||||
3,LG新能源,韩国,52.6,8.6,8.4,608.5
|
||||
4,中创新航,中国,31.2,5.1,39.5,608.5
|
||||
5,国轩高科,中国,28.0,4.6,43.3,608.5
|
||||
6,松下,日本,22.7,3.7,10.2,608.5
|
||||
7,亿纬锂能,中国,20.9,3.4,51.7,608.5
|
||||
8,SK On,韩国,19.0,3.1,-6.7,608.5
|
||||
9,蜂巢能源,中国,15.7,2.6,40.9,608.5
|
||||
10,欣旺达,中国,14.5,2.4,13.7,608.5
|
||||
|
@@ -0,0 +1,7 @@
|
||||
period,catl,byd,lges,panasonic,sk_on,samsung_sdi,calb,guoxuan,eve,sunwoda,svolt,farasis,global_total
|
||||
2021,99.5,26.4,59.4,36.3,17.3,14.5,8.0,6.7,,2.6,,2.4,301.5
|
||||
2022,191.6,70.4,70.4,38.0,27.8,24.3,20.0,14.1,,9.2,,7.4,517.9
|
||||
2023,259.7,111.4,95.8,42.8,34.7,33.1,33.4,17.1,16.2,10.5,,,705.5
|
||||
2024,339.3,153.7,96.3,35.1,39.0,29.6,39.4,28.5,20.3,18.8,,,894.4
|
||||
2025,464.7,194.8,108.8,44.2,44.5,28.9,62.8,53.5,31.3,,28.5,,1187.0
|
||||
2026H1,242.7,87.7,52.6,22.7,19.0,,31.2,28.0,20.9,14.5,15.7,,608.5
|
||||
|
@@ -0,0 +1,7 @@
|
||||
period,catl,byd,lges,panasonic,sk_on,samsung_sdi,calb,guoxuan,eve,sunwoda,svolt,farasis,cr3,cr5,cr10,china_top10_count,china_share
|
||||
2021,33.0,8.7,19.7,12.0,5.7,4.8,2.6,2.2,,0.9,,0.8,64.7,79.1,90.4,6,48.2
|
||||
2022,37.0,13.6,13.6,7.3,5.4,4.7,3.9,2.7,,1.8,,1.4,64.2,76.9,91.4,6,60.4
|
||||
2023,36.8,15.8,13.6,6.1,4.9,4.7,4.7,2.4,2.3,1.5,,,66.2,77.2,92.8,6,63.5
|
||||
2024,37.9,17.2,10.8,3.9,4.4,3.3,4.4,3.2,2.3,2.1,,,65.9,74.7,89.5,6,67.1
|
||||
2025,39.2,16.4,9.2,3.7,3.7,2.4,5.3,4.5,2.6,,2.4,,64.8,74.6,89.4,6,70.4
|
||||
2026H1,39.9,14.4,8.6,3.7,3.1,,5.1,4.6,3.4,2.4,2.6,,62.9,72.6,87.8,7,72.4
|
||||
|
@@ -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,online,robot_store,wholesale,total_revenue
|
||||
2024H1,57.4,25.9,8.1,8.6,45.59
|
||||
2024H2,49.0,35.2,5.4,10.4,84.78
|
||||
2024FY,51.9,31.9,6.4,9.8,130.37
|
||||
2025H1,49.8,39.6,5.9,4.7,138.75
|
||||
2025H2,44.5,47.0,3.7,4.7,232.45
|
||||
2025FY,46.5,44.3,4.6,4.7,371.20
|
||||
2026H1,53.1,36.2,5.5,5.2,171.74
|
||||
|
@@ -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,3 @@
|
||||
period,retail,online,robot_store,wholesale,total_revenue
|
||||
2024FY,51.9,31.9,6.4,9.8,130.37
|
||||
2025FY,46.5,44.3,4.6,4.7,371.20
|
||||
|
@@ -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
|
||||
|
@@ -0,0 +1,6 @@
|
||||
period,retail,online,robot_store,wholesale,total_revenue
|
||||
2024H1,57.4,25.9,8.1,8.6,45.59
|
||||
2024H2,49.0,35.2,5.4,10.4,84.78
|
||||
2025H1,49.8,39.6,5.9,4.7,138.75
|
||||
2025H2,44.5,47.0,3.7,4.7,232.45
|
||||
2026H1,53.1,36.2,5.5,5.2,171.74
|
||||
|
@@ -1,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
|
||||
|
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,22 @@
|
||||
# 泡泡玛特财报分析底稿
|
||||
|
||||
本目录是泡泡玛特的唯一维护数据层。日常只维护 [泡泡玛特_财报分析底稿.xlsx](泡泡玛特_财报分析底稿.xlsx):图表使用的 `data/ppmt/*.csv` 由脚本生成,不直接编辑。
|
||||
|
||||
## 工作簿结构
|
||||
|
||||
- `说明`:维护入口、口径和颜色约定。
|
||||
- `资产负债表`:期末时点的财报明细。
|
||||
- `利润表`:半年累计与全年累计的财报明细。
|
||||
- `现金流量表`:现金流量表披露的原始明细。
|
||||
- `资产分类规则`:把财报明细映射为“类现金、投资资产、有息负债”等分析分类的规则;不维护分类金额。
|
||||
- `检查`:资产负债、现金流量的勾稽差额。出现红色警告时,应回到来源核对,不能为了消除警告而改数。
|
||||
|
||||
## 更新流程
|
||||
|
||||
1. 将新财报 PDF、截图或原始表格保留在 `data/ppmt/raw/`,作为证据文件。
|
||||
2. 由 AI 读取证据后,将直接披露的科目、期间和金额录入本工作簿的蓝色数字区域,并记录来源定位。
|
||||
3. 检查期间、单位和勾稽关系;确认后再生成图表 CSV 与图表。
|
||||
|
||||
只维护基础值:收入、门店数、财报科目等。同比、占比、利润率、H2 单半年和分析分类小计都由后续脚本计算。若财报另有披露占比,可作为核对依据,不应成为第二套手工维护的数据。
|
||||
|
||||
期间约定:`FY` 是全年累计/年末,`H1` 是上半年累计/六月末;不得把来源的 `H2` 直接当作下半年单期。金额单位为人民币亿元。
|
||||
@@ -0,0 +1,146 @@
|
||||
region,channel,period,revenue
|
||||
中国,零售店,2024H1,20.04
|
||||
中国,零售店,2024FY,46.01
|
||||
中国,零售店,2025H1,44.06
|
||||
中国,零售店,2025FY,100.75
|
||||
中国,零售店,2026H1,60.90
|
||||
中国,机器人商店,2024H1,3.38
|
||||
中国,机器人商店,2024FY,7.51
|
||||
中国,机器人商店,2025H1,6.78
|
||||
中国,机器人商店,2025FY,13.46
|
||||
中国,机器人商店,2026H1,7.79
|
||||
中国,线上渠道,2024H1,9.41
|
||||
中国,线上渠道,2024FY,27.72
|
||||
中国,线上渠道,2025H1,29.37
|
||||
中国,线上渠道,2025FY,85.22
|
||||
中国,线上渠道,2026H1,47.79
|
||||
中国,泡泡玛特抽盒机,2024H1,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
|
||||
中国,批发及其他,2025FY,9.08
|
||||
中国,批发及其他,2026H1,5.53
|
||||
亚太,零售店,2024H1,4.85
|
||||
亚太,零售店,2024FY,16.68
|
||||
亚太,零售店,2025H1,14.93
|
||||
亚太,零售店,2025FY,43.19
|
||||
亚太,零售店,2026H1,17.16
|
||||
亚太,机器人商店,2024H1,0.17
|
||||
亚太,机器人商店,2024FY,0.40
|
||||
亚太,机器人商店,2025H1,0.33
|
||||
亚太,机器人商店,2025FY,0.95
|
||||
亚太,机器人商店,2026H1,0.57
|
||||
亚太,线上渠道,2024H1,1.65
|
||||
亚太,线上渠道,2024FY,9.72
|
||||
亚太,线上渠道,2025H1,10.69
|
||||
亚太,线上渠道,2025FY,30.53
|
||||
亚太,线上渠道,2026H1,6.44
|
||||
亚太,自研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
|
||||
亚太,批发及其他,2025FY,5.44
|
||||
亚太,批发及其他,2026H1,1.59
|
||||
美洲,零售店,2024H1,0.87
|
||||
美洲,零售店,2024FY,3.41
|
||||
美洲,零售店,2025H1,7.37
|
||||
美洲,零售店,2025FY,20.04
|
||||
美洲,零售店,2026H1,9.03
|
||||
美洲,机器人商店,2024H1,0.13
|
||||
美洲,机器人商店,2024FY,0.38
|
||||
美洲,机器人商店,2025H1,1.05
|
||||
美洲,机器人商店,2025FY,2.32
|
||||
美洲,机器人商店,2026H1,1.04
|
||||
美洲,线上渠道,2024H1,0.64
|
||||
美洲,线上渠道,2024FY,3.64
|
||||
美洲,线上渠道,2025H1,13.27
|
||||
美洲,线上渠道,2025FY,43.54
|
||||
美洲,线上渠道,2026H1,7.22
|
||||
美洲,自研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
|
||||
美洲,批发及其他,2025FY,2.17
|
||||
美洲,批发及其他,2026H1,1.62
|
||||
欧洲及其他,零售店,2024H1,0.41
|
||||
欧洲及其他,零售店,2024FY,1.57
|
||||
欧洲及其他,零售店,2025H1,2.72
|
||||
欧洲及其他,零售店,2025FY,8.56
|
||||
欧洲及其他,零售店,2026H1,4.03
|
||||
欧洲及其他,机器人商店,2024H1,0.01
|
||||
欧洲及其他,机器人商店,2024FY,0.02
|
||||
欧洲及其他,机器人商店,2025H1,0.04
|
||||
欧洲及其他,机器人商店,2025FY,0.17
|
||||
欧洲及其他,机器人商店,2026H1,0.11
|
||||
欧洲及其他,线上渠道,2024H1,0.11
|
||||
欧洲及其他,线上渠道,2024FY,0.55
|
||||
欧洲及其他,线上渠道,2025H1,1.65
|
||||
欧洲及其他,线上渠道,2025FY,5.05
|
||||
欧洲及其他,线上渠道,2026H1,0.68
|
||||
欧洲及其他,自研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
|
||||
欧洲及其他,批发及其他,2025FY,0.73
|
||||
欧洲及其他,批发及其他,2026H1,0.24
|
||||
|
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,36 @@
|
||||
# 由 scripts/build_sweet_spot_catl.py 生成,请勿手工编辑。
|
||||
# 口径:总股本 46.27 亿股 · 净现金 2,263.8 亿元(每股 48.93 元)· 2026E 归母 900/964/1,000 亿元 · 分红率 50% · 现价 316.21 元(2026-09-15)
|
||||
# PE 与股息率保留两位小数(见脚本 docstring 的精度例外说明)
|
||||
price,market_cap,pe_bear,pe_base,pe_bull,pe_ex_cash,dividend_yield,net_cash_pct
|
||||
220.00,10179.4,11.31,10.56,10.18,8.21,4.74,22.2
|
||||
240.00,11104.8,12.34,11.52,11.10,9.17,4.34,20.4
|
||||
260.00,12030.2,13.37,12.48,12.03,10.13,4.01,18.8
|
||||
265.00,12261.6,13.62,12.72,12.26,10.37,3.93,18.5
|
||||
280.00,12955.6,14.40,13.44,12.96,11.09,3.72,17.5
|
||||
290.00,13418.3,14.91,13.92,13.42,11.57,3.59,16.9
|
||||
300.00,13881.0,15.42,14.40,13.88,12.05,3.47,16.3
|
||||
303.00,14019.8,15.58,14.54,14.02,12.20,3.44,16.1
|
||||
316.21,14631.0,16.26,15.18,14.63,12.83,3.29,15.5
|
||||
320.00,14806.4,16.45,15.36,14.81,13.01,3.26,15.3
|
||||
340.00,15731.8,17.48,16.32,15.73,13.97,3.06,14.4
|
||||
360.00,16657.2,18.51,17.28,16.66,14.93,2.89,13.6
|
||||
378.00,17490.1,19.43,18.14,17.49,15.79,2.76,12.9
|
||||
380.00,17582.6,19.54,18.24,17.58,15.89,2.74,12.9
|
||||
400.00,18508.0,20.56,19.20,18.51,16.85,2.60,12.2
|
||||
420.00,19433.4,21.59,20.16,19.43,17.81,2.48,11.6
|
||||
440.00,20358.8,22.62,21.12,20.36,18.77,2.37,11.1
|
||||
460.00,21284.2,23.65,22.08,21.28,19.73,2.26,10.6
|
||||
480.00,22209.6,24.68,23.04,22.21,20.69,2.17,10.2
|
||||
500.00,23135.0,25.71,24.00,23.14,21.65,2.08,9.8
|
||||
520.00,24060.4,26.73,24.96,24.06,22.61,2.00,9.4
|
||||
540.00,24985.8,27.76,25.92,24.99,23.57,1.93,9.1
|
||||
560.00,25911.2,28.79,26.88,25.91,24.53,1.86,8.7
|
||||
580.00,26836.6,29.82,27.84,26.84,25.49,1.80,8.4
|
||||
600.00,27762.0,30.85,28.80,27.76,26.45,1.74,8.2
|
||||
605.00,27993.4,31.10,29.04,27.99,26.69,1.72,8.1
|
||||
617.00,28548.6,31.72,29.61,28.55,27.27,1.69,7.9
|
||||
620.00,28687.4,31.87,29.76,28.69,27.41,1.68,7.9
|
||||
640.00,29612.8,32.90,30.72,29.61,28.37,1.63,7.6
|
||||
660.00,30538.2,33.93,31.68,30.54,29.33,1.58,7.4
|
||||
680.00,31463.6,34.96,32.64,31.46,30.29,1.53,7.2
|
||||
700.00,32389.0,35.99,33.60,32.39,31.25,1.49,7.0
|
||||
|
@@ -0,0 +1,36 @@
|
||||
# 由 scripts/build_sweet_spot_catl.py 生成,请勿手工编辑。
|
||||
# 口径:总股本 46.27 亿股 · 净现金 2,263.8 亿元(每股 48.93 元)· 2026E 归母 900/964/1,000 亿元 · 分红率 50% · 现价 316.21 元(2026-09-15)
|
||||
# PE 与股息率保留两位小数(见脚本 docstring 的精度例外说明)
|
||||
price,market_cap,pe_base,dividend_yield,cagr_price,cagr_total
|
||||
220.00,10179.4,10.56,4.74,40.1,46.7
|
||||
240.00,11104.8,11.52,4.34,36.1,42.0
|
||||
260.00,12030.2,12.48,4.01,32.5,37.8
|
||||
265.00,12261.6,12.72,3.93,31.7,36.9
|
||||
280.00,12955.6,13.44,3.72,29.3,34.1
|
||||
290.00,13418.3,13.92,3.59,27.8,32.4
|
||||
300.00,13881.0,14.40,3.47,26.3,30.7
|
||||
303.00,14019.8,14.54,3.44,25.9,30.3
|
||||
316.21,14631.0,15.18,3.29,24.1,28.2
|
||||
320.00,14806.4,15.36,3.26,23.7,27.7
|
||||
340.00,15731.8,16.32,3.06,21.2,24.9
|
||||
360.00,16657.2,17.28,2.89,18.9,22.3
|
||||
378.00,17490.1,18.14,2.76,17.0,20.2
|
||||
380.00,17582.6,18.24,2.74,16.8,20.0
|
||||
400.00,18508.0,19.20,2.60,14.8,17.8
|
||||
420.00,19433.4,20.16,2.48,12.9,15.7
|
||||
440.00,20358.8,21.12,2.37,11.2,13.8
|
||||
450.00,20821.5,21.60,2.31,10.4,12.9
|
||||
460.00,21284.2,22.08,2.26,9.6,12.0
|
||||
480.00,22209.6,23.04,2.17,8.0,10.4
|
||||
500.00,23135.0,24.00,2.08,6.6,8.8
|
||||
520.00,24060.4,24.96,2.00,5.2,7.3
|
||||
540.00,24985.8,25.92,1.93,3.9,5.9
|
||||
560.00,25911.2,26.88,1.86,2.6,4.5
|
||||
580.00,26836.6,27.84,1.80,1.4,3.2
|
||||
600.00,27762.0,28.80,1.74,0.3,2.0
|
||||
605.00,27993.4,29.04,1.72,0.0,1.7
|
||||
620.00,28687.4,29.76,1.68,-0.8,0.9
|
||||
640.00,29612.8,30.72,1.63,-1.9,-0.3
|
||||
660.00,30538.2,31.68,1.58,-2.9,-1.3
|
||||
680.00,31463.6,32.64,1.53,-3.8,-2.3
|
||||
700.00,32389.0,33.60,1.49,-4.7,-3.3
|
||||
|
@@ -0,0 +1,30 @@
|
||||
# 由 scripts/build_sweet_spot_tencent.py 生成,请勿手工编辑。
|
||||
# 口径:总股本 90.95 亿股 · 汇率 0.86154 · 非经营资产 5,841.8 亿元(净现金 581.9 + 上市投资 4,872.0 + 非上市投资 387.9)· TTM EBIT 2,585.43 亿元 · 三情景 EBIT 2,400/2,585/2,900 亿元 · 现价 426.8 港元(2026-09-17)
|
||||
# 倍数与年化收益保留两位小数(与宁德两份表同精度;倍数一位会把 10.68x 与 10.7x 揉成一个读数)
|
||||
price,market_cap,mult_bear,mult_base,mult_bull,mult_net_cash,mult_gross,non_op_pct
|
||||
240.00,18805.7,5.40,5.01,4.47,7.05,7.27,31.1
|
||||
260.00,20372.8,6.05,5.62,5.01,7.65,7.88,28.7
|
||||
280.00,21940.0,6.71,6.23,5.55,8.26,8.49,26.6
|
||||
300.00,23507.1,7.36,6.83,6.09,8.87,9.09,24.9
|
||||
320.00,25074.3,8.01,7.44,6.63,9.47,9.70,23.3
|
||||
338.50,26523.9,8.62,8.00,7.13,10.03,10.26,22.0
|
||||
340.00,26641.4,8.67,8.04,7.17,10.08,10.30,21.9
|
||||
360.00,28208.5,9.32,8.65,7.71,10.69,10.91,20.7
|
||||
371.50,29109.6,9.69,9.00,8.02,11.03,11.26,20.1
|
||||
380.00,29775.7,9.97,9.26,8.25,11.29,11.52,19.6
|
||||
400.00,31342.8,10.63,9.86,8.79,11.90,12.12,18.6
|
||||
404.50,31695.4,10.77,10.00,8.92,12.03,12.26,18.4
|
||||
420.00,32910.0,11.28,10.47,9.33,12.50,12.73,17.8
|
||||
426.80,33442.8,11.50,10.68,9.52,12.71,12.94,17.5
|
||||
440.00,34477.1,11.93,11.08,9.87,13.11,13.34,16.9
|
||||
460.00,36044.2,12.58,11.68,10.41,13.72,13.94,16.2
|
||||
470.50,36867.0,12.93,12.00,10.70,14.03,14.26,15.8
|
||||
480.00,37611.4,13.24,12.29,10.96,14.32,14.55,15.5
|
||||
500.00,39178.5,13.89,12.89,11.50,14.93,15.15,14.9
|
||||
520.00,40745.7,14.54,13.50,12.04,15.53,15.76,14.3
|
||||
540.00,42312.8,15.20,14.11,12.58,16.14,16.37,13.8
|
||||
560.00,43880.0,15.85,14.71,13.12,16.75,16.97,13.3
|
||||
580.00,45447.1,16.50,15.32,13.66,17.35,17.58,12.9
|
||||
600.00,47014.2,17.16,15.92,14.20,17.96,18.18,12.4
|
||||
620.00,48581.4,17.81,16.53,14.74,18.57,18.79,12.0
|
||||
640.00,50148.5,18.46,17.14,15.28,19.17,19.40,11.6
|
||||
|
@@ -0,0 +1,30 @@
|
||||
# 由 scripts/build_sweet_spot_tencent.py 生成,请勿手工编辑。
|
||||
# 口径:总股本 90.95 亿股 · 汇率 0.86154 · 非经营资产 5,841.8 亿元(净现金 581.9 + 上市投资 4,872.0 + 非上市投资 387.9)· TTM EBIT 2,585.43 亿元 · 三情景 EBIT 2,400/2,585/2,900 亿元 · 现价 426.8 港元(2026-09-17)
|
||||
# 倍数与年化收益保留两位小数(与宁德两份表同精度;倍数一位会把 10.68x 与 10.7x 揉成一个读数)
|
||||
price,market_cap,mult_base,cagr_12x,cagr_10x,cagr_12x_dep
|
||||
240.00,18805.7,5.01,25.16,19.01,20.38
|
||||
260.00,20372.8,5.62,21.86,15.87,17.21
|
||||
280.00,21940.0,6.23,18.89,13.05,14.35
|
||||
300.00,23507.1,6.83,16.18,10.48,11.75
|
||||
320.00,25074.3,7.44,13.71,8.12,9.37
|
||||
338.50,26523.9,8.00,11.60,6.12,7.34
|
||||
340.00,26641.4,8.04,11.44,5.96,7.18
|
||||
360.00,28208.5,8.65,9.33,3.96,5.16
|
||||
371.50,29109.6,9.00,8.19,2.88,4.06
|
||||
380.00,29775.7,9.26,7.38,2.11,3.28
|
||||
400.00,31342.8,9.86,5.56,0.37,1.53
|
||||
404.50,31695.4,10.00,5.17,0.00,1.15
|
||||
420.00,32910.0,10.47,3.86,-1.24,-0.11
|
||||
426.80,33442.8,10.68,3.30,-1.77,-0.64
|
||||
440.00,34477.1,11.08,2.26,-2.76,-1.64
|
||||
460.00,36044.2,11.68,0.76,-4.19,-3.09
|
||||
470.50,36867.0,12.00,-0.00,-4.91,-3.82
|
||||
480.00,37611.4,12.29,-0.66,-5.54,-4.45
|
||||
500.00,39178.5,12.89,-2.01,-6.82,-5.75
|
||||
520.00,40745.7,13.50,-3.28,-8.03,-6.97
|
||||
540.00,42312.8,14.11,-4.49,-9.18,-8.13
|
||||
560.00,43880.0,14.71,-5.64,-10.28,-9.24
|
||||
580.00,45447.1,15.32,-6.74,-11.32,-10.30
|
||||
600.00,47014.2,15.92,-7.78,-12.31,-11.30
|
||||
620.00,48581.4,16.53,-8.79,-13.27,-12.27
|
||||
640.00,50148.5,17.14,-9.75,-14.18,-13.19
|
||||
|
@@ -0,0 +1,138 @@
|
||||
# 图表导出为图片:实现说明
|
||||
|
||||
> 记录 `smart-fin-chart` 的图片下载是如何实现的、命名从哪来、以及一个重要的对照结论:
|
||||
> **同一段下载代码,在真实浏览器顶层页面能下载,在被 sandbox 限制的 iframe 里会被静默拦截。**
|
||||
|
||||
---
|
||||
|
||||
## 一、两条导出路径
|
||||
|
||||
项目里有两种导出方式,都落在浏览器原生能力上,**没有任何第三方库**(没有 file-saver、没有 html2canvas)。
|
||||
|
||||
### 路径 A:单张图表导出
|
||||
|
||||
| 环节 | 位置 |
|
||||
|---|---|
|
||||
| 按钮 | `js/ui.js:197` —— 卡片右上角 `导出 PNG` |
|
||||
| 取图 | `js/main.js:265 exportPNG()` —— `instance.getDataURL({ type: 'png', pixelRatio: 2, backgroundColor: '#fff' })` |
|
||||
| 下载 | 同一个函数里 `anchor.href = dataURL` → `anchor.download = '${filename}.png'` → `anchor.click()` |
|
||||
|
||||
关键点:
|
||||
- 直接用 **ECharts 实例自带的 `getDataURL()`**,等于把画布按 2 倍像素密度重新导出一遍,**不截屏、不重绘**,所以导出结果和处理中的图完全一致,且天然是高清的(`pixelRatio: 2`)。
|
||||
- `backgroundColor: '#fff'` 是必须的 —— 否则透明底在公众号里会变黑/变灰。
|
||||
|
||||
### 路径 B:多张合并成长图
|
||||
|
||||
入口是卡片上的 `➕ 加入合并`(`js/ui.js:191`),形成一个「合并队列」,然后走三步:
|
||||
|
||||
1. **入队即存快照**(`js/main.js:282 toggleCart`)
|
||||
```js
|
||||
cart.set(chart.instanceId, {
|
||||
instanceId, title,
|
||||
filename: chart.filename || chart.title,
|
||||
dataUrl: instance.getDataURL({ type: 'png', pixelRatio: 2, backgroundColor: '#fff' }),
|
||||
});
|
||||
```
|
||||
存的是**入队那一刻的 dataURL**,不是 ECharts 实例引用。这样即使用户随后切换视图(全部/年度/半年)、改了参数、甚至窗口 resize,队列里的图也不会变。`captureSnapshot()`(`js/main.js:275`)负责在重渲染后刷新快照,保证「所见即所得」。
|
||||
|
||||
2. **Canvas 纵向拼接**(`js/merge.js:23 mergeImages`)
|
||||
- 宽度取各图最大值,宽度不一致的等比缩放对齐;
|
||||
- 高度 = 各图高度之和 + `gap × (张数 - 1)`,`gap = 96`(@2x 下视觉 48px);
|
||||
- 先铺白底,再逐张 `ctx.drawImage()`。
|
||||
- 为什么要纵向拼:公众号在手机上以竖版窗口预览,横版单图显示过小;两张拼成 2400×~2800 接近满屏。
|
||||
|
||||
3. **Blob 下载**(`js/merge.js:48 downloadCanvas`)
|
||||
```js
|
||||
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');
|
||||
```
|
||||
代码注释里写明了理由:**Blob URL 比超长 dataURL 下载更稳** —— 合并长图的 dataURL 字符串可能到几 MB 量级,塞进 `href` 在部分浏览器上会失败;转成 Blob 再由浏览器托管,稳定得多。用完 10 秒后释放,避免内存泄漏。
|
||||
|
||||
注意:**单图路径用的是 dataURL,合并路径用的是 Blob URL** —— 这不是不一致,是刻意分工(单图 dataURL 只有几百 KB,够用且更简单)。
|
||||
|
||||
### 合并预览弹窗
|
||||
|
||||
`showMergePreview()`(`js/ui.js:276`)只是在 canvas 上直接显示 + 挂两个下载按钮,**不产生第二份数据**:`entry.canvas` 就是拼好的那块 canvas,点下载才走 `toBlob`。
|
||||
|
||||
- 配对模式:队列两两合并(`buildCompositions()`,`js/main.js:324`),奇数张时最后一张单独导出并提示;
|
||||
- 「全部下载」用 `setTimeout(..., index * 350)` 错峰触发(`js/main.js:353`)—— 连续同步触发多个下载会被浏览器判定为「自动批量下载」而拦截。
|
||||
|
||||
---
|
||||
|
||||
## 二、文件名从哪来
|
||||
|
||||
**统一来自 `manifest.json` 的 `filename` 字段,回退到 `chart.title`。**
|
||||
|
||||
- 挂载点:`js/ui.js:198` → `onExport(chart.instanceId, chart.filename || chart.title)`
|
||||
- `manifest.json` 里共 **85 处** `filename`,例如:
|
||||
```json
|
||||
{ "key": "ppmt-profit", "title": "盈利能力", ..., "filename": "泡泡玛特盈利能力" }
|
||||
```
|
||||
- 也就是说**展示名和下载名是两套**:卡片标题是短的(「盈利能力」),下载名是完整的(「泡泡玛特盈利能力」)。这样屏幕上不啰嗦,存下来的一堆图又不会重名。
|
||||
- 合并导出时用 `clipName()` 截断到 24 字,多张用 `+` 连接(`js/main.js:319, 338`)。
|
||||
|
||||
---
|
||||
|
||||
## 三、对照结论:为什么同一段代码在别处就下载不了
|
||||
|
||||
**这个项目能下载,根本原因是它跑在真实浏览器的顶层页面**(`start.command` → `http://localhost:8899/` → `open "$URL"`),**不在任何 iframe 沙箱里**。
|
||||
|
||||
对照实验(无头 Chromium + CDP 显式 `Browser.setDownloadBehavior: allow`,两组跑**完全同一段**下载代码):
|
||||
|
||||
| 场景 | 结果 | CDP 事件 |
|
||||
|---|---|---|
|
||||
| 顶层页面 | ✅ 文件落盘 | `Browser.downloadWillBegin` → `downloadProgress` ×6 |
|
||||
| `sandbox="allow-scripts allow-popups allow-popups-to-escape-sandbox allow-forms allow-modals"` 的 iframe | ❌ 未落盘 | **连 `downloadWillBegin` 都没有** |
|
||||
|
||||
**第二条是要害**:沙箱拦截时**不报错、不弹提示、不发事件** —— 下载请求在发起前就被丢弃。页面上看不出任何异常,控制台也没有告警,所以表现成「按钮点了没反应」,而且回溯起来会误以为是代码写错了。
|
||||
|
||||
结论:
|
||||
1. **沙箱 iframe 缺 `allow-downloads`** 是唯一原因,和 `toBlob` / `createObjectURL` / `a.click()` 的写法无关;
|
||||
2. 补救方向是让下载动作**离开沙箱**:宿主允许 `allow-popups-to-escape-sandbox` 时,从 iframe 里 `window.open()` 出一个弹窗(弹窗不受沙箱约束),在弹窗里挂 `<a download>` 触发下载。
|
||||
3. 注意 `window.open()` **必须有真实用户手势**(真实点击),脚本内直接调用会被弹窗拦截器挡掉。
|
||||
|
||||
---
|
||||
|
||||
## 四、可复用的最小实现
|
||||
|
||||
```js
|
||||
// canvas → PNG 下载(Blob URL 版本,适合大图)
|
||||
function downloadCanvas(canvas, filename) {
|
||||
canvas.toBlob((blob) => {
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = filename.endsWith('.png') ? filename : `${filename}.png`;
|
||||
a.click();
|
||||
setTimeout(() => URL.revokeObjectURL(url), 10_000);
|
||||
}, 'image/png');
|
||||
}
|
||||
|
||||
// ECharts 实例 → PNG 下载(高清,dataURL 版本)
|
||||
function downloadChart(chartInstance, filename) {
|
||||
const a = document.createElement('a');
|
||||
a.href = chartInstance.getDataURL({ type: 'png', pixelRatio: 2, backgroundColor: '#fff' });
|
||||
a.download = `${filename}.png`;
|
||||
a.click();
|
||||
}
|
||||
```
|
||||
|
||||
前提:**这段代码必须在非沙箱的顶层页面里执行**。若运行在 iframe 中,先确认宿主给了 `allow-downloads`,否则要走弹窗逃逸。
|
||||
|
||||
---
|
||||
|
||||
## 五、相关文件索引
|
||||
|
||||
| 文件 | 职责 |
|
||||
|---|---|
|
||||
| `js/merge.js` | `mergeImages()` 纵向拼接、`downloadCanvas()` Blob 下载 |
|
||||
| `js/main.js` | `exportPNG()` 单图导出、`toggleCart()` 队列快照、`buildCompositions()` 配对、`openMergePreview()` |
|
||||
| `js/ui.js` | `renderCards()` 挂导出按钮、`showMergePreview()` 预览弹窗 |
|
||||
| `manifest.json` | `filename` 字段(下载命名,85 处) |
|
||||
| `css/style.css` | `.export-button` / `.cart-*` / `.modal-*` / `.preview-*` 样式 |
|
||||
@@ -0,0 +1,172 @@
|
||||
# 财报图表项目数据架构改造方案
|
||||
|
||||
> 状态:建议实施
|
||||
>
|
||||
> 目标:保持静态图表站点的低复杂度,同时建立可追溯、可重复生成的数据链路。
|
||||
|
||||
## 1. 决策
|
||||
|
||||
当前不建设在线数据库和后端 API。
|
||||
|
||||
本项目目前是静态数据展示工具:浏览器按需读取 CSV,数据量很小,没有多人并发写入、权限控制、运行时查询或服务端计算的需求。此时引入 Postgres、API、鉴权和迁移机制会增加运维成本,但不能解决当前的数据正确性与可维护性问题。
|
||||
|
||||
CSV 保留为图表的运行时交付格式;需要改造的是 CSV 之前的数据生产链路。
|
||||
|
||||
## 2. 当前问题
|
||||
|
||||
### 2.1 权威数据源不一致
|
||||
|
||||
设计文档和 `data/ppmt/rawData/README.md` 将 `财报分析底稿.xlsx` 定义为唯一维护入口,但 `scripts/normalize_ppmt_financials.mjs` 实际读取的是 `data/ppmt/raw/*.csv`。底稿更新不会稳定地传递到图表。
|
||||
|
||||
### 2.2 派生产物被当作输入
|
||||
|
||||
归一化脚本的一部分通过 `readRuntimeRows()` 读取 `data/ppmt/*.csv`,再生成年度、半年等视图。IP、品类、资产等数据不能完全从上游重新构建;运行时 CSV 被删除、手改或过期时,会影响后续结果。
|
||||
|
||||
### 2.3 数据更新不完整
|
||||
|
||||
已出现底稿含有新期间、但相应图表 CSV 尚未更新的情况。问题不在某个数值,而在于链路没有可重复的全量构建保证。
|
||||
|
||||
### 2.4 发布边界不清晰
|
||||
|
||||
Vite 目前递归复制 `data/` 下的全部 CSV,因此原始披露 CSV 也会进入 `dist/`。原始证据、维护输入和公开运行时数据应有物理边界。
|
||||
|
||||
### 2.5 现有测试只验证展示
|
||||
|
||||
`pnpm test` 已能验证 manifest、CSV 可读性、图表渲染和部分视觉规范;还没有验证期间、单位、勾稽、同比口径、来源定位或派生逻辑。
|
||||
|
||||
## 3. 目标架构
|
||||
|
||||
```text
|
||||
原始披露文件(PDF / XLSX / 原始导出 CSV,只读证据)
|
||||
│
|
||||
▼
|
||||
人工录入与复核的规范化基础事实层(唯一权威)
|
||||
│ 数据构建:映射、同比、期间拆分、勾稽、图表数据整形
|
||||
▼
|
||||
图表运行时 CSV(完全可重建,不允许手工修改)
|
||||
│
|
||||
▼
|
||||
静态站点:manifest + Vanilla JS + ECharts
|
||||
```
|
||||
|
||||
### 3.1 各层职责
|
||||
|
||||
| 层 | 内容 | 是否人工编辑 | 发布到网站 |
|
||||
| --- | --- | --- | --- |
|
||||
| 原始证据 | 财报 PDF、公司 XLSX、原始导出 CSV、截图 | 否 | 否 |
|
||||
| 基础事实层 | 直接披露的数值、期间、单位、来源定位、科目映射 | 是,受控编辑 | 否 |
|
||||
| 派生层 | H2、同比、利润率、分类汇总、年度/半年视图 | 否,脚本生成 | 否 |
|
||||
| 图表 CSV | 仅包含图表需要的字段和布局 | 否,脚本生成 | 是 |
|
||||
|
||||
## 4. 基础事实层如何维护
|
||||
|
||||
基础事实层需要人工维护,但人工只编辑直接披露且可追溯的事实。
|
||||
|
||||
允许人工编辑的内容:
|
||||
|
||||
- 原始财报直接披露的金额、门店数等基础数值;
|
||||
- 期间、币种、单位、合并口径;
|
||||
- 来源文件、页码、表格、行号或单元格定位;
|
||||
- 科目映射与分类规则,例如“类现金”“投资资产”“经营资产”。
|
||||
|
||||
禁止人工编辑的内容:
|
||||
|
||||
- 同比、环比、占比、利润率;
|
||||
- `H2 = FY - H1` 等期间拆分;
|
||||
- 分析分类小计;
|
||||
- 任何图表运行时 CSV。
|
||||
|
||||
现有 `财报分析底稿.xlsx` 可以继续作为人工录入界面。改造后的要求是:构建脚本必须从这份工作簿或其一次性导出的规范化输入文件读取,不再并行从其他位置读取同一事实。工作簿是编辑器,不是与其他 CSV 并列的第二权威来源。
|
||||
|
||||
基础事实建议采用“长表”结构,便于复用、校验和未来接入多家公司:
|
||||
|
||||
| company_id | metric_id | period_end | period_type | value | unit | source_file | source_locator | revision |
|
||||
| --- | --- | --- | --- | ---: | --- | --- | --- | --- |
|
||||
| ppmt | revenue | 2026-06-30 | H1_YTD | 171.73 | CNY_100M | 2026H1.pdf | p.12 / 利润表 / 营业收入 | 1 |
|
||||
|
||||
运营类事实可使用相同结构,或按需要增加 `dimension_type`、`dimension_value`,例如地区、渠道、IP 和品类。
|
||||
|
||||
## 5. 目录与构建边界
|
||||
|
||||
建议将数据目录拆分,避免将证据文件误发布:
|
||||
|
||||
```text
|
||||
data/
|
||||
ppmt/
|
||||
evidence/ # 原始披露文件,只读,不发布
|
||||
input/ # 基础事实与映射规则,受控维护,不发布
|
||||
generated/ # 构建中间产物,可删除并重建,不发布
|
||||
public/
|
||||
data/
|
||||
ppmt/ # 仅图表运行时 CSV,发布
|
||||
scripts/
|
||||
import_ppmt_facts.* # 从 XLSX 或受控输入导入基础事实
|
||||
build_ppmt_datasets.* # 从基础事实完整生成图表 CSV
|
||||
check_ppmt_data.* # 质量与勾稽校验
|
||||
```
|
||||
|
||||
Vite 仅复制 `public/`;不再以“复制整个 `data/` 再过滤扩展名”的方式发布文件。
|
||||
|
||||
## 6. 数据构建与校验
|
||||
|
||||
推荐提供三个明确命令:
|
||||
|
||||
```bash
|
||||
pnpm data:import # 可选:从人工维护的 XLSX 导出/读取基础事实
|
||||
pnpm data:build # 从唯一权威源全量生成 generated 与 public/data
|
||||
pnpm data:check # 校验输入、派生结果与勾稽关系
|
||||
pnpm test # 图表渲染和展示回归
|
||||
pnpm build # 静态站点构建
|
||||
```
|
||||
|
||||
`data:build` 必须满足幂等性:在清空 `generated/` 和 `public/data/` 后,仍能只凭 `input/` 与 `evidence/` 完整生成相同的图表数据。构建脚本不得读取自身输出目录作为输入。
|
||||
|
||||
`data:check` 至少覆盖:
|
||||
|
||||
- 必填字段、数值类型、单位和币种;
|
||||
- 期间格式、排序、重复与缺失;
|
||||
- 流量类 `H2 = FY - H1`,时点类 H2/年末口径;
|
||||
- 同比仅比较同口径上一年期间;
|
||||
- 资产 = 负债 + 权益、现金流勾稽等适用检查;
|
||||
- 图表字段 schema、空值约束和合理数值范围;
|
||||
- 每条人工事实都有来源定位。
|
||||
|
||||
## 7. 分阶段实施
|
||||
|
||||
### P0:先恢复单一真相源
|
||||
|
||||
1. 决定以现有底稿 XLSX 作为人工维护入口。
|
||||
2. 为底稿建立稳定导入器,输出规范化基础事实;或将其导出为版本化输入 CSV。
|
||||
3. 将现有脚本中读取运行时 CSV 的逻辑改为读取基础事实。
|
||||
4. 将图表 CSV 移到仅发布目录,原始证据移出发布路径。
|
||||
5. 增加 `data:check` 的最小校验与全量重建测试。
|
||||
|
||||
完成标准:删除全部图表 CSV 后,仍能从唯一输入源重建所有图表;底稿新增期间后,数据检查能明确指出哪些图表应更新。
|
||||
|
||||
### P1:第二家公司接入后抽象共性
|
||||
|
||||
1. 固化跨公司通用的基础事实 schema、期间模型、来源定位格式和校验规则。
|
||||
2. 仅在两个公司确实复用同一字段映射与口径时,抽取通用图表或数据模板。
|
||||
3. 为 manifest 增加可选的数据契约版本,避免图表代码默默依赖 CSV 列名。
|
||||
|
||||
### P2:需要分析型查询时引入本地数据库
|
||||
|
||||
当需要跨公司指标筛选、较多事实数据查询、复杂聚合或批量分析时,引入 DuckDB + Parquet 作为本地构建和分析层。它可以替代或补强 `generated/`,但前端仍然发布静态 CSV 或预聚合 JSON。
|
||||
|
||||
### P3:满足以下条件才建设在线数据库
|
||||
|
||||
仅在出现多人在线录入、权限分级、审计版本、自动数据抓取、实时更新或面向用户的任意组合查询时,再建设 Postgres + 后端 API。
|
||||
|
||||
在线数据库只替换“基础事实层的存储与协作方式”;图表运行时仍应优先使用缓存后的静态数据集或受版本控制的 API 响应,不应让每张图直接查询底层事实表。
|
||||
|
||||
## 8. 数据库选择原则
|
||||
|
||||
“使用数据库”不是替代数据治理的方案。无论存储介质是 XLSX、CSV、DuckDB 还是 Postgres,都必须先满足:
|
||||
|
||||
1. 只有一个权威输入源;
|
||||
2. 派生产物可从该输入源完整重建;
|
||||
3. 每个数值可回溯到披露来源;
|
||||
4. 口径、单位和期间语义经过自动校验;
|
||||
5. 前端只消费稳定的数据契约。
|
||||
|
||||
在当前规模下,最优路线是“XLSX 受控人工录入 + 规范化事实 + 自动生成 CSV + 静态发布”,而不是立即迁移在线数据库。
|
||||
+8
-4
@@ -23,10 +23,13 @@
|
||||
## 2. 当前架构
|
||||
|
||||
```
|
||||
原始财报 / PDF / XLSX(仓库外保存)
|
||||
│ 人工、AI 或脚本:抽取、归一化、口径确认
|
||||
原始财报 / PDF / XLSX / 截图(证据文件)
|
||||
│ 人工或 AI 提取、复核并记录来源定位
|
||||
▼
|
||||
data/{company}/*.csv ← 运行时数据
|
||||
data/{company}/rawData/财报分析底稿.xlsx ← 财报分析底稿:唯一可维护的数据层
|
||||
│ 脚本:分类、同比/占比、年度/半年视图、勾稽
|
||||
▼
|
||||
data/{company}/*.csv ← 图表运行时数据,可随时重建,禁止手工编辑
|
||||
▼
|
||||
manifest.json ← 展示编排与图表配置的唯一事实来源
|
||||
├─ chartCatalog ← 每张图只定义一次
|
||||
@@ -69,7 +72,8 @@ js/main.js + js/ui.js ← 解析引用、渲染三栏、导出和
|
||||
- `js/charts/index.js`:注册 `key → build 函数`。
|
||||
- `js/charts/*.js`:只处理该图的业务字段与 ECharts 表达。
|
||||
- `js/theme.js`:字体、配色、图例、坐标轴、脚注和水印等统一视觉规范。
|
||||
- `data/{company}/`:图表需要的规范化 CSV。
|
||||
- `data/{company}/rawData/`:财报分析底稿。每家公司以一份可读、可维护的工作簿作为入口;基础科目、期间、单位、来源文件/工作表/定位信息在此维护,分析分类通过独立规则映射,不复制维护分类金额。
|
||||
- `data/{company}/*.csv`:图表需要的规范化 CSV,由底稿生成,不是维护入口。
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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知行记'),
|
||||
],
|
||||
};
|
||||
}
|
||||
+1040
File diff suppressed because it is too large
Load Diff
+1
-1
@@ -72,7 +72,7 @@ export function growthChart(rows, { width, height }) {
|
||||
],
|
||||
graphic: [
|
||||
...watermarks(width, height),
|
||||
footnote('示例数据 | @思考的Joey'),
|
||||
footnote('示例数据 | @Joey知行记'),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
+77
-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';
|
||||
@@ -25,6 +24,8 @@ import { cashflowCompositionChart } from './ppmt_cashflow_composition.js';
|
||||
import { workingCapitalDaysChart } from './ppmt_working_capital_days.js';
|
||||
import { expenseStructureChart } from './ppmt_expense_structure.js';
|
||||
import { channelRevenueChart } from './ppmt_channel_revenue.js';
|
||||
import { channelShareChart } from './ppmt_channel_share.js';
|
||||
import { 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';
|
||||
@@ -32,6 +33,29 @@ 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';
|
||||
import {
|
||||
peerInstallChart, peerUtilizationChart, peerShareChart,
|
||||
peerGpPerWhChart, peerAspChart, peerMarginChart, peer2026h1Chart,
|
||||
peerGpGapChart, peerGpChangeChart, catlUnitEconomicsChart, catlVolumeGpChart,
|
||||
globalCrChart, globalTop10Chart, globalRegionShareChart,
|
||||
} from './catl.js';
|
||||
import {
|
||||
sweetCatlPricePeChart, sweetCatlNetCashChart, sweetCatlAnyPriceChart,
|
||||
sweetTencentPriceEvChart, sweetTencentNonOpChart, sweetTencentAnyPriceChart,
|
||||
} from './sweet_spot.js';
|
||||
|
||||
/** 图表构建函数注册表。manifest 只引用这里的 key,不直接引用 JS 函数。 */
|
||||
export const chartRegistry = {
|
||||
@@ -45,7 +69,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,
|
||||
@@ -62,6 +85,15 @@ export const chartRegistry = {
|
||||
'ppmt-working-capital-days': workingCapitalDaysChart,
|
||||
'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,
|
||||
@@ -69,6 +101,49 @@ 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,
|
||||
'catl-unit-economics': catlUnitEconomicsChart,
|
||||
'catl-volume-gp': catlVolumeGpChart,
|
||||
'peer-install': peerInstallChart,
|
||||
'peer-utilization': peerUtilizationChart,
|
||||
'peer-share': peerShareChart,
|
||||
'peer-gp-per-wh': peerGpPerWhChart,
|
||||
'peer-asp': peerAspChart,
|
||||
'peer-margin': peerMarginChart,
|
||||
'peer-2026h1': peer2026h1Chart,
|
||||
'peer-gp-gap': peerGpGapChart,
|
||||
'peer-gp-change': peerGpChangeChart,
|
||||
'global-cr': globalCrChart,
|
||||
'global-top10': globalTop10Chart,
|
||||
'global-region-share': globalRegionShareChart,
|
||||
'sweet-catl-price-pe': sweetCatlPricePeChart,
|
||||
'sweet-catl-net-cash': sweetCatlNetCashChart,
|
||||
'sweet-catl-any-price': sweetCatlAnyPriceChart,
|
||||
'sweet-tencent-price-ev': sweetTencentPriceEvChart,
|
||||
'sweet-tencent-non-op': sweetTencentNonOpChart,
|
||||
'sweet-tencent-any-price': sweetTencentAnyPriceChart,
|
||||
};
|
||||
|
||||
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知行记'),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
import { col } from '../csv.js';
|
||||
import {
|
||||
CHANNEL_COLORS, PALETTE, FONT, axisCommon, standalonePeriodAxis, titleStyle, standardLegend,
|
||||
financialViewLabel, barSeries, stackedSharePoint, fanTinyLabelOffsets, footnote, watermarks, LAYOUT,
|
||||
} from '../theme.js';
|
||||
|
||||
const CHANNELS = [
|
||||
{ key: 'retail', name: '零售店', color: CHANNEL_COLORS.retail, labelColor: '#fff' },
|
||||
{ key: 'online', name: '线上渠道', color: CHANNEL_COLORS.online, labelColor: '#fff' },
|
||||
{ key: 'robot_store', name: '机器人商店', color: CHANNEL_COLORS.robotStore, labelColor: '#fff' },
|
||||
{ key: 'wholesale', name: '批发及其他', color: CHANNEL_COLORS.wholesale, labelColor: PALETTE.ink },
|
||||
];
|
||||
|
||||
/** 泡泡玛特全渠道收入占比:全部地区合并,每根柱按当期总收入归一化为 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',
|
||||
title: {
|
||||
...titleStyle,
|
||||
text: `泡泡玛特渠道收入占比(${financialViewLabel(view)})`,
|
||||
subtext: '全部地区合并;归一化堆叠:每根柱为当期总收入的 100%',
|
||||
},
|
||||
legend: standardLegend(CHANNELS.map((item) => item.name)),
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: { type: 'shadow' },
|
||||
textStyle: { fontFamily: FONT },
|
||||
formatter: (params) => {
|
||||
const row = rows[periods.indexOf(params[0].axisValue)];
|
||||
return `<b>${params[0].axisValue}</b> 总收入 ${row.total_revenue.toFixed(2)} 亿元<br/>${params
|
||||
.filter((item) => item.value !== null)
|
||||
.slice()
|
||||
.reverse()
|
||||
.map((item) => `${item.marker}${item.seriesName}: ${item.value.toFixed(1)}%`)
|
||||
.join('<br/>')}`;
|
||||
},
|
||||
},
|
||||
grid: { left: 78, right: 42, top: LAYOUT.gridTop, bottom: 100 },
|
||||
xAxis: { ...standalonePeriodAxis, data: periods },
|
||||
yAxis: {
|
||||
...axisCommon,
|
||||
type: 'value',
|
||||
min: 0,
|
||||
max: 100,
|
||||
interval: 20,
|
||||
axisLabel: { ...axisCommon.axisLabel, formatter: '{value}%' },
|
||||
},
|
||||
series: CHANNELS.map((item, ci) => barSeries({
|
||||
name: item.name,
|
||||
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',
|
||||
label: {
|
||||
show: true,
|
||||
position: 'inside',
|
||||
fontSize: 11,
|
||||
fontFamily: FONT,
|
||||
fontWeight: 500,
|
||||
formatter: (point) => `${point.value.toFixed(1)}%`,
|
||||
},
|
||||
emphasis: { focus: 'series' },
|
||||
})),
|
||||
graphic: [
|
||||
...watermarks(width, height),
|
||||
footnote('数据来源:泡泡玛特分地区渠道收入披露(中国含港澳台);H2为单半年(全年−H1)\n线上渠道含抽盒机、天猫、抖音、自研APP及官网等 | @Joey知行记'),
|
||||
],
|
||||
};
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user