Compare commits
5 Commits
7aab3d7134
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| b3c7c30a1f | |||
| 2555453b79 | |||
| d51890a450 | |||
| d010b107aa | |||
| e5c21c6148 |
@@ -6,8 +6,8 @@
|
|||||||
|
|
||||||
```js
|
```js
|
||||||
import {
|
import {
|
||||||
PALETTE, FONT, axisCommon, periodAxis, titleStyle,
|
PALETTE, FINANCIAL_COLORS, BRIDGE_COLORS, CHANNEL_COLORS, FONT, axisCommon, periodAxis, titleStyle,
|
||||||
lineSeries, barSeries, standardLegend, footnote, watermarks, LAYOUT,
|
lineSeries, lineDataWithSafeLabels, barSeries, standardLegend, footnote, watermarks, LAYOUT,
|
||||||
} from '../theme.js';
|
} from '../theme.js';
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -27,13 +27,13 @@ pnpm test # 全量,只在改了 t
|
|||||||
|
|
||||||
全量跑纯属浪费,而且十几张无关图表的输出会盖住本次改动的真实问题。不传参数时才是全量,所以 `pnpm test` 保持向后兼容。
|
全量跑纯属浪费,而且十几张无关图表的输出会盖住本次改动的真实问题。不传参数时才是全量,所以 `pnpm test` 保持向后兼容。
|
||||||
|
|
||||||
它会断言 `legend.top`、`grid.top`、折线 `symbolSize`/`borderWidth`,并检查 SVG 字体引号。加新图记得同步往 `regress.mjs` 顶部的 `all` 数组加一行,否则不会被测到。
|
它会断言 `legend.top`、`grid.top`、折线 `symbolSize`/`borderWidth`,并检查 SVG 字体引号。回归测试直接遍历 `manifest.json` 的 `chartCatalog`,新增图只要完成 registry 与 manifest 登记就会被测到。
|
||||||
|
|
||||||
**右侧纵向图例的图会报 `legend.top=middle (期望85)`,这是预期的,不要去"修"。** 那条断言的 85px 基准是给顶部横向图例定的,对 `orient: 'vertical'` + `top: 'middle'` 的图不适用(`ppmt_ip_share`、`ppmt_ip_share_history` 都会报)。
|
**右侧纵向图例的图会报 `legend.top=middle (期望85)`,这是预期的,不要去"修"。** 那条断言的 85px 基准是给顶部横向图例定的,对 `orient: 'vertical'` + `top: 'middle'` 的图不适用(`ppmt_ip_share`、`ppmt_ip_share_history` 都会报)。
|
||||||
|
|
||||||
## 踩过的坑
|
## 踩过的坑
|
||||||
|
|
||||||
**不要用 `...legendStyle`。** 它遗留 `top: 62` 且曾带 `icon: 'roundRect'`——后者会盖掉 ECharts 按系列类型自动生成的图例(折线本该是「短线+空心圆」)。用 `standardLegend()`。
|
**优先用 `standardLegend()`,且不要手工设置 `legend.icon`。** ECharts 会按系列类型自动生成图例:柱状图为矩形,折线图为「短线 + 空心圆」。需要特殊位置时可扩展 `legendStyle`,但不得覆盖这一自动类型图标。
|
||||||
|
|
||||||
**`label.position` 不接受函数。** 写了会被静默忽略、退化成默认位置,实测确认过。要让同一系列内不同点朝向不同,只能逐点覆盖 `data`:`{ value, label: { position: 'top' } }`。
|
**`label.position` 不接受函数。** 写了会被静默忽略、退化成默认位置,实测确认过。要让同一系列内不同点朝向不同,只能逐点覆盖 `data`:`{ value, label: { position: 'top' } }`。
|
||||||
|
|
||||||
@@ -59,6 +59,12 @@ pnpm test # 全量,只在改了 t
|
|||||||
|
|
||||||
## 配色
|
## 配色
|
||||||
|
|
||||||
|
**正式方向:轻快消费。** 柱线组合图的现金流、利润与同比使用 `FINANCIAL_COLORS`,渠道结构使用 `CHANNEL_COLORS`,瀑布图使用 `BRIDGE_COLORS`(蓝色基准、青绿色正向、珊瑚红负向);不要在图表文件里为相同财务/渠道语义重写十六进制颜色。柱状系列负责分类,折线只保留一条高辨识度强调色。
|
||||||
|
|
||||||
|
**一张图只留一个视觉主角。** 辅助柱使用中等饱和度;避免红、橙、紫等多个强调色同时竞争。分类色要同时拉开色相和明度;折线除颜色外还使用线宽、实虚线、空心圆和标签优先级辅助区分。
|
||||||
|
|
||||||
|
**柱线组合的标签不能压在柱体端点。** 使用 `lineDataWithSafeLabels(rows, { lineKey, barKeys })`:正值标签在上、负值标签在下,接近柱值时自动增加距离,并配合 ECharts `labelLayout` 兜底;不要按某个年份写特判。
|
||||||
|
|
||||||
先分清两件事,别混用同一个门槛。**选色的功夫要和系列数成正比**——两个系列的图不该选半天。
|
先分清两件事,别混用同一个门槛。**选色的功夫要和系列数成正比**——两个系列的图不该选半天。
|
||||||
|
|
||||||
**① 同一张图内的系列要能互相区分。** 这条永远要满足,是可读性问题:
|
**① 同一张图内的系列要能互相区分。** 这条永远要满足,是可读性问题:
|
||||||
|
|||||||
+33
-5
@@ -123,6 +123,9 @@ button {
|
|||||||
.content { min-width: 0; }
|
.content { min-width: 0; }
|
||||||
|
|
||||||
.tabs {
|
.tabs {
|
||||||
|
position: sticky;
|
||||||
|
top: 20px;
|
||||||
|
z-index: 5;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
@@ -142,7 +145,7 @@ button {
|
|||||||
background: var(--surface);
|
background: var(--surface);
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
box-shadow: 0 1px 3px rgba(0, 0, 0, .06);
|
box-shadow: 0 1px 3px rgba(0, 0, 0, .06);
|
||||||
scroll-margin-top: 20px;
|
scroll-margin-top: 84px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-bar {
|
.card-bar {
|
||||||
@@ -198,14 +201,39 @@ button {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 880px) {
|
@media (max-width: 880px) {
|
||||||
.app-layout { display: block; padding: 16px 12px 40px; }
|
.app-layout { display: block; padding: 0 12px 40px; }
|
||||||
.sidebar { position: static; margin-bottom: 12px; }
|
.sidebar { margin-bottom: 12px; }
|
||||||
.company-sidebar { padding: 7px; }
|
.company-sidebar {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 7;
|
||||||
|
margin: 0 -12px;
|
||||||
|
padding: 7px 12px;
|
||||||
|
border-width: 0 0 1px;
|
||||||
|
border-radius: 0;
|
||||||
|
background: rgba(245, 244, 241, .96);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
}
|
||||||
.company-sidebar .sidebar-title { display: none; }
|
.company-sidebar .sidebar-title { display: none; }
|
||||||
.company-list { display: flex; overflow-x: auto; }
|
.company-list { display: flex; overflow-x: auto; }
|
||||||
.company-button { flex: 0 0 auto; width: auto; }
|
.company-button { flex: 0 0 auto; width: auto; }
|
||||||
.tabs { overflow-x: auto; flex-wrap: nowrap; }
|
.tabs {
|
||||||
|
top: 50px;
|
||||||
|
z-index: 6;
|
||||||
|
overflow-x: auto;
|
||||||
|
overflow-y: hidden;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
margin: 0 -12px 18px;
|
||||||
|
padding: 5px 12px;
|
||||||
|
border-width: 0 0 1px;
|
||||||
|
border-radius: 0;
|
||||||
|
background: rgba(245, 244, 241, .96);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
scrollbar-width: none;
|
||||||
|
}
|
||||||
|
.tabs::-webkit-scrollbar { display: none; }
|
||||||
.tab-button { flex: 0 0 auto; }
|
.tab-button { flex: 0 0 auto; }
|
||||||
|
.card { scroll-margin-top: 120px; }
|
||||||
.card-bar { align-items: flex-start; flex-wrap: wrap; }
|
.card-bar { align-items: flex-start; flex-wrap: wrap; }
|
||||||
.export-button { margin-left: auto; }
|
.export-button { margin-left: auto; }
|
||||||
.chart { min-width: 560px; }
|
.chart { min-width: 560px; }
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
period,total_assets,cash_like,receivables,investment,operating_assets
|
period,total_assets,cash_like,receivables,investment,operating_assets
|
||||||
2020H1,17.89,8.22,0.41,0.46,8.8
|
2020H1,17.89,8.22,0.41,0.46,8.80
|
||||||
2020H2,69.71,56.8,0.78,0.67,11.45
|
2020H2,69.71,56.80,0.78,0.67,11.45
|
||||||
2021H1,76.41,57.67,1.35,1.78,15.6
|
2021H1,76.41,57.67,1.35,1.78,15.60
|
||||||
2021H2,83.24,52.65,1.71,4.11,24.77
|
2021H2,83.24,52.65,1.71,4.11,24.77
|
||||||
2022H1,85.9,51.63,1.32,5.53,27.42
|
2022H1,85.90,51.63,1.32,5.53,27.42
|
||||||
2022H2,85.8,50.42,1.94,5.55,27.89
|
2022H2,85.80,50.42,1.94,5.55,27.89
|
||||||
2023H1,90.65,55.4,2.09,5.75,27.42
|
2023H1,90.65,55.40,2.09,5.75,27.42
|
||||||
2023H2,99.69,59.63,3.21,5.87,30.97
|
2023H2,99.69,59.63,3.21,5.87,30.97
|
||||||
2024H1,110.17,70.1,2.64,5.5,31.93
|
2024H1,110.17,70.10,2.64,5.50,31.93
|
||||||
2024H2,148.71,96.2,4.78,5.6,42.13
|
2024H2,148.71,96.20,4.78,5.60,42.13
|
||||||
2025H1,213.57,137.66,9.72,5.4,60.79
|
2025H1,213.57,137.66,9.72,5.40,60.79
|
||||||
2025H2,321.01,172.25,9.21,4.95,132.04
|
2025H2,321.01,172.25,9.21,4.95,132.04
|
||||||
|
|||||||
|
@@ -0,0 +1,7 @@
|
|||||||
|
period,total_assets,cash_like,receivables,investment,operating_assets
|
||||||
|
2020FY,69.71,56.80,0.78,0.67,11.45
|
||||||
|
2021FY,83.24,52.65,1.71,4.11,24.77
|
||||||
|
2022FY,85.80,50.42,1.94,5.55,27.89
|
||||||
|
2023FY,99.69,59.63,3.21,5.87,30.97
|
||||||
|
2024FY,148.71,96.20,4.78,5.60,42.13
|
||||||
|
2025FY,321.01,172.25,9.21,4.95,132.04
|
||||||
|
@@ -0,0 +1,13 @@
|
|||||||
|
period,total_assets,cash_like,receivables,investment,operating_assets
|
||||||
|
2020H1,17.89,8.22,0.41,0.46,8.80
|
||||||
|
2020H2,69.71,56.80,0.78,0.67,11.45
|
||||||
|
2021H1,76.41,57.67,1.35,1.78,15.60
|
||||||
|
2021H2,83.24,52.65,1.71,4.11,24.77
|
||||||
|
2022H1,85.90,51.63,1.32,5.53,27.42
|
||||||
|
2022H2,85.80,50.42,1.94,5.55,27.89
|
||||||
|
2023H1,90.65,55.40,2.09,5.75,27.42
|
||||||
|
2023H2,99.69,59.63,3.21,5.87,30.97
|
||||||
|
2024H1,110.17,70.10,2.64,5.50,31.93
|
||||||
|
2024H2,148.71,96.20,4.78,5.60,42.13
|
||||||
|
2025H1,213.57,137.66,9.72,5.40,60.79
|
||||||
|
2025H2,321.01,172.25,9.21,4.95,132.04
|
||||||
|
@@ -1,7 +1,19 @@
|
|||||||
period,operating_cash_flow,net_profit,cash_conversion
|
period,operating_cash_flow,net_profit,cash_conversion
|
||||||
|
2020H1,1.22,1.41,86.5
|
||||||
|
2020H2,5.81,3.82,152.1
|
||||||
2020FY,7.03,5.23,134.4
|
2020FY,7.03,5.23,134.4
|
||||||
|
2021H1,4.42,3.59,123.1
|
||||||
|
2021H2,3.37,4.96,68.1
|
||||||
2021FY,7.79,8.55,91.2
|
2021FY,7.79,8.55,91.2
|
||||||
|
2022H1,3.74,3.33,112.2
|
||||||
|
2022H2,5.17,1.43,362.2
|
||||||
2022FY,8.91,4.76,187.3
|
2022FY,8.91,4.76,187.3
|
||||||
|
2023H1,10.92,4.77,228.9
|
||||||
|
2023H2,8.98,6.12,146.9
|
||||||
2023FY,19.91,10.89,182.8
|
2023FY,19.91,10.89,182.8
|
||||||
|
2024H1,19.05,9.64,197.6
|
||||||
|
2024H2,30.49,23.44,130.1
|
||||||
2024FY,49.54,33.08,149.7
|
2024FY,49.54,33.08,149.7
|
||||||
|
2025H1,59.77,46.82,127.7
|
||||||
|
2025H2,48.88,83.30,58.7
|
||||||
2025FY,108.65,130.12,83.5
|
2025FY,108.65,130.12,83.5
|
||||||
|
|||||||
|
@@ -0,0 +1,7 @@
|
|||||||
|
period,operating_cash_flow,net_profit,cash_conversion
|
||||||
|
2020FY,7.03,5.23,134.4
|
||||||
|
2021FY,7.79,8.55,91.2
|
||||||
|
2022FY,8.91,4.76,187.3
|
||||||
|
2023FY,19.91,10.89,182.8
|
||||||
|
2024FY,49.54,33.08,149.7
|
||||||
|
2025FY,108.65,130.12,83.5
|
||||||
|
@@ -0,0 +1,13 @@
|
|||||||
|
period,operating_cash_flow,net_profit,cash_conversion
|
||||||
|
2020H1,1.22,1.41,86.5
|
||||||
|
2020H2,5.81,3.82,152.1
|
||||||
|
2021H1,4.42,3.59,123.1
|
||||||
|
2021H2,3.37,4.96,68.1
|
||||||
|
2022H1,3.74,3.33,112.2
|
||||||
|
2022H2,5.17,1.43,362.2
|
||||||
|
2023H1,10.92,4.77,228.9
|
||||||
|
2023H2,8.98,6.12,146.9
|
||||||
|
2024H1,19.05,9.64,197.6
|
||||||
|
2024H2,30.49,23.44,130.1
|
||||||
|
2025H1,59.77,46.82,127.7
|
||||||
|
2025H2,48.88,83.30,58.7
|
||||||
|
@@ -1,7 +1,19 @@
|
|||||||
period,operating_cash_flow,investing_cash_flow,financing_cash_flow,net_cash_increase
|
period,operating_cash_flow,investing_cash_flow,financing_cash_flow,net_cash_increase
|
||||||
|
2020H1,1.22,-0.48,4.24,4.98
|
||||||
|
2020H2,5.81,-1.69,44.47,48.59
|
||||||
2020FY,7.03,-2.17,48.70,53.57
|
2020FY,7.03,-2.17,48.70,53.57
|
||||||
|
2021H1,4.42,-44.98,-1.21,-41.78
|
||||||
|
2021H2,3.37,38.50,-3.15,38.73
|
||||||
2021FY,7.79,-6.48,-4.36,-3.05
|
2021FY,7.79,-6.48,-4.36,-3.05
|
||||||
|
2022H1,3.74,-42.10,-4.81,-43.17
|
||||||
|
2022H2,5.17,-4.89,-6.97,-6.68
|
||||||
2022FY,8.91,-46.98,-11.77,-49.85
|
2022FY,8.91,-46.98,-11.77,-49.85
|
||||||
|
2023H1,10.92,0.82,-5.58,6.16
|
||||||
|
2023H2,8.98,1.52,-2.83,7.67
|
||||||
2023FY,19.91,2.34,-8.42,13.83
|
2023FY,19.91,2.34,-8.42,13.83
|
||||||
|
2024H1,19.05,3.34,-7.16,15.23
|
||||||
|
2024H2,30.49,-3.25,-2.43,24.82
|
||||||
2024FY,49.54,0.09,-9.59,40.05
|
2024FY,49.54,0.09,-9.59,40.05
|
||||||
|
2025H1,59.77,12.34,-14.05,58.07
|
||||||
|
2025H2,48.88,-22.23,-3.62,23.02
|
||||||
2025FY,108.65,-9.89,-17.67,81.09
|
2025FY,108.65,-9.89,-17.67,81.09
|
||||||
|
|||||||
|
@@ -0,0 +1,7 @@
|
|||||||
|
period,operating_cash_flow,investing_cash_flow,financing_cash_flow,net_cash_increase
|
||||||
|
2020FY,7.03,-2.17,48.70,53.57
|
||||||
|
2021FY,7.79,-6.48,-4.36,-3.05
|
||||||
|
2022FY,8.91,-46.98,-11.77,-49.85
|
||||||
|
2023FY,19.91,2.34,-8.42,13.83
|
||||||
|
2024FY,49.54,0.09,-9.59,40.05
|
||||||
|
2025FY,108.65,-9.89,-17.67,81.09
|
||||||
|
@@ -0,0 +1,13 @@
|
|||||||
|
period,operating_cash_flow,investing_cash_flow,financing_cash_flow,net_cash_increase
|
||||||
|
2020H1,1.22,-0.48,4.24,4.98
|
||||||
|
2020H2,5.81,-1.69,44.47,48.59
|
||||||
|
2021H1,4.42,-44.98,-1.21,-41.78
|
||||||
|
2021H2,3.37,38.50,-3.15,38.73
|
||||||
|
2022H1,3.74,-42.10,-4.81,-43.17
|
||||||
|
2022H2,5.17,-4.89,-6.97,-6.68
|
||||||
|
2023H1,10.92,0.82,-5.58,6.16
|
||||||
|
2023H2,8.98,1.52,-2.83,7.67
|
||||||
|
2024H1,19.05,3.34,-7.16,15.23
|
||||||
|
2024H2,30.49,-3.25,-2.43,24.82
|
||||||
|
2025H1,59.77,12.34,-14.05,58.07
|
||||||
|
2025H2,48.88,-22.23,-3.62,23.02
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
period,plush,figure,mega,other
|
period,plush,figure,mega,other
|
||||||
品类,毛绒,手办,MEGA,衍生品及其他
|
2024H1,4.46,26.57,5.86,8.69
|
||||||
2024H1,4.46105,26.56781,5.8613,8.68815
|
2024H2,23.86,42.79,10.98,7.17
|
||||||
2024H2,23.85973,42.79306,10.9765,7.16989
|
2024FY,28.32,69.36,16.84,15.86
|
||||||
2025H1,61.39236,51.75875,10.07147,15.54018
|
2025H1,61.39,51.76,10.07,15.54
|
||||||
2025H2,125.68838,68.46694,9.08935,29.19309
|
2025H2,125.69,68.47,9.09,29.19
|
||||||
|
2025FY,187.08,120.23,19.16,44.73
|
||||||
|
|||||||
|
@@ -0,0 +1,3 @@
|
|||||||
|
period,plush,figure,mega,other
|
||||||
|
2024FY,28.32,69.36,16.84,15.86
|
||||||
|
2025FY,187.08,120.23,19.16,44.73
|
||||||
|
@@ -0,0 +1,5 @@
|
|||||||
|
period,plush,figure,mega,other
|
||||||
|
2024H1,4.46,26.57,5.86,8.69
|
||||||
|
2024H2,23.86,42.79,10.98,7.17
|
||||||
|
2025H1,61.39,51.76,10.07,15.54
|
||||||
|
2025H2,125.69,68.47,9.09,29.19
|
||||||
|
@@ -1,14 +1,20 @@
|
|||||||
period,retail,online,robot_store,wholesale,total_revenue,revenue_yoy
|
period,retail,online,robot_store,wholesale,total_revenue,revenue_yoy
|
||||||
2020H1,3.13,3.34,1.05,0.64,8.18,50.5
|
2020H1,3.13,3.34,1.05,0.64,8.18,50.5
|
||||||
|
2020H2,6.89,6.18,2.23,1.44,16.96,48.7
|
||||||
2020FY,10.02,9.52,3.29,2.07,25.13,49.3
|
2020FY,10.02,9.52,3.29,2.07,25.13,49.3
|
||||||
2021H1,6.75,6.78,2.28,1.91,17.73,116.8
|
2021H1,6.75,6.78,2.28,1.91,17.73,116.8
|
||||||
|
2021H2,9.98,12.01,2.42,2.53,27.18,60.3
|
||||||
2021FY,16.73,18.79,4.70,4.45,44.91,78.7
|
2021FY,16.73,18.79,4.70,4.45,44.91,78.7
|
||||||
2022H1,9.26,10.13,2.08,2.12,23.59,33.1
|
2022H1,9.26,10.13,2.08,2.12,23.59,33.1
|
||||||
|
2022H2,9.00,9.07,1.86,2.65,22.59,-16.9
|
||||||
2022FY,18.27,19.20,3.94,4.77,46.17,2.8
|
2022FY,18.27,19.20,3.94,4.77,46.17,2.8
|
||||||
2023H1,13.44,8.74,2.96,2.99,28.14,19.3
|
2023H1,13.44,8.74,2.96,2.99,28.14,19.3
|
||||||
|
2023H2,17.18,9.92,3.10,4.68,34.87,54.4
|
||||||
2023FY,30.62,18.66,6.06,7.67,63.01,36.5
|
2023FY,30.62,18.66,6.06,7.67,63.01,36.5
|
||||||
2024H1,23.65,13.43,3.68,4.82,45.58,62.0
|
2024H1,23.65,13.43,3.68,4.82,45.58,62.0
|
||||||
|
2024H2,44.01,28.10,4.63,8.06,84.80,143.2
|
||||||
2024FY,67.66,41.53,8.31,12.88,130.38,106.9
|
2024FY,67.66,41.53,8.31,12.88,130.38,106.9
|
||||||
2025H1,69.08,54.98,8.21,6.50,138.76,204.4
|
2025H1,69.08,54.98,8.21,6.50,138.76,204.4
|
||||||
|
2025H2,103.46,109.36,8.69,10.93,232.44,174.1
|
||||||
2025FY,172.54,164.34,16.89,17.42,371.20,184.7
|
2025FY,172.54,164.34,16.89,17.42,371.20,184.7
|
||||||
2026H1,91.12,62.12,9.51,8.98,171.73,23.8
|
2026H1,91.12,62.12,9.51,8.98,171.73,23.8
|
||||||
|
|||||||
|
@@ -0,0 +1,7 @@
|
|||||||
|
period,retail,online,robot_store,wholesale,total_revenue,revenue_yoy
|
||||||
|
2020FY,10.02,9.52,3.29,2.07,25.13,49.3
|
||||||
|
2021FY,16.73,18.79,4.70,4.45,44.91,78.7
|
||||||
|
2022FY,18.27,19.20,3.94,4.77,46.17,2.8
|
||||||
|
2023FY,30.62,18.66,6.06,7.67,63.01,36.5
|
||||||
|
2024FY,67.66,41.53,8.31,12.88,130.38,106.9
|
||||||
|
2025FY,172.54,164.34,16.89,17.42,371.20,184.7
|
||||||
|
@@ -0,0 +1,14 @@
|
|||||||
|
period,retail,online,robot_store,wholesale,total_revenue,revenue_yoy
|
||||||
|
2020H1,3.13,3.34,1.05,0.64,8.18,50.5
|
||||||
|
2020H2,6.89,6.18,2.23,1.44,16.96,48.7
|
||||||
|
2021H1,6.75,6.78,2.28,1.91,17.73,116.8
|
||||||
|
2021H2,9.98,12.01,2.42,2.53,27.18,60.3
|
||||||
|
2022H1,9.26,10.13,2.08,2.12,23.59,33.1
|
||||||
|
2022H2,9.00,9.07,1.86,2.65,22.59,-16.9
|
||||||
|
2023H1,13.44,8.74,2.96,2.99,28.14,19.3
|
||||||
|
2023H2,17.18,9.92,3.10,4.68,34.87,54.4
|
||||||
|
2024H1,23.65,13.43,3.68,4.82,45.58,62.0
|
||||||
|
2024H2,44.01,28.10,4.63,8.06,84.80,143.2
|
||||||
|
2025H1,69.08,54.98,8.21,6.50,138.76,204.4
|
||||||
|
2025H2,103.46,109.36,8.69,10.93,232.44,174.1
|
||||||
|
2026H1,91.12,62.12,9.51,8.98,171.73,23.8
|
||||||
|
@@ -1,14 +1,20 @@
|
|||||||
period,core_margin
|
period,core_margin
|
||||||
2020H1,22.6
|
2020H1,22.6
|
||||||
2020H2,27.2
|
2020H2,29.4
|
||||||
|
2020FY,27.2
|
||||||
2021H1,25.8
|
2021H1,25.8
|
||||||
2021H2,24.4
|
2021H2,23.5
|
||||||
|
2021FY,24.4
|
||||||
2022H1,15.0
|
2022H1,15.0
|
||||||
2022H2,10.8
|
2022H2,6.3
|
||||||
|
2022FY,10.8
|
||||||
2023H1,17.4
|
2023H1,17.4
|
||||||
2023H2,18.3
|
2023H2,19.0
|
||||||
|
2023FY,18.3
|
||||||
2024H1,24.8
|
2024H1,24.8
|
||||||
2024H2,31.5
|
2024H2,35.1
|
||||||
|
2024FY,31.5
|
||||||
2025H1,41.8
|
2025H1,41.8
|
||||||
2025H2,45.6
|
2025H2,47.8
|
||||||
|
2025FY,45.6
|
||||||
2026H1,41.4
|
2026H1,41.4
|
||||||
|
|||||||
|
@@ -0,0 +1,7 @@
|
|||||||
|
period,core_margin
|
||||||
|
2020FY,27.2
|
||||||
|
2021FY,24.4
|
||||||
|
2022FY,10.8
|
||||||
|
2023FY,18.3
|
||||||
|
2024FY,31.5
|
||||||
|
2025FY,45.6
|
||||||
|
@@ -0,0 +1,14 @@
|
|||||||
|
period,core_margin
|
||||||
|
2020H1,22.6
|
||||||
|
2020H2,29.4
|
||||||
|
2021H1,25.8
|
||||||
|
2021H2,23.5
|
||||||
|
2022H1,15.0
|
||||||
|
2022H2,6.3
|
||||||
|
2023H1,17.4
|
||||||
|
2023H2,19.0
|
||||||
|
2024H1,24.8
|
||||||
|
2024H2,35.1
|
||||||
|
2025H1,41.8
|
||||||
|
2025H2,47.8
|
||||||
|
2026H1,41.4
|
||||||
|
@@ -1,14 +1,20 @@
|
|||||||
period,core_profit,yoy
|
period,core_profit,yoy
|
||||||
2020H1,1.9,25.0
|
2020H1,1.85,25.2
|
||||||
2020H2,6.8,17.1
|
2020H2,4.99,14.4
|
||||||
2021H1,4.6,147.0
|
2020FY,6.84,17.1
|
||||||
2021H2,11.0,60.1
|
2021H1,4.58,147.3
|
||||||
2022H1,3.5,-22.5
|
2021H2,6.37,27.7
|
||||||
2022H2,5.0,-54.5
|
2021FY,10.95,60.1
|
||||||
2023H1,4.9,38.1
|
2022H1,3.54,-22.5
|
||||||
2023H2,11.5,131.3
|
2022H2,1.43,-77.6
|
||||||
2024H1,11.3,131.5
|
2022FY,4.98,-54.6
|
||||||
2024H2,41.1,256.9
|
2023H1,4.89,37.9
|
||||||
2025H1,58.0,412.2
|
2023H2,6.63,363.8
|
||||||
2025H2,169.1,311.4
|
2023FY,11.52,131.6
|
||||||
2026H1,71.1,22.6
|
2024H1,11.31,131.5
|
||||||
|
2024H2,29.79,349.1
|
||||||
|
2024FY,41.10,256.8
|
||||||
|
2025H1,57.98,412.4
|
||||||
|
2025H2,111.14,273.1
|
||||||
|
2025FY,169.12,311.5
|
||||||
|
2026H1,71.08,22.6
|
||||||
|
|||||||
|
@@ -0,0 +1,7 @@
|
|||||||
|
period,core_profit,yoy
|
||||||
|
2020FY,6.84,17.1
|
||||||
|
2021FY,10.95,60.1
|
||||||
|
2022FY,4.98,-54.6
|
||||||
|
2023FY,11.52,131.6
|
||||||
|
2024FY,41.10,256.8
|
||||||
|
2025FY,169.12,311.5
|
||||||
|
@@ -0,0 +1,14 @@
|
|||||||
|
period,core_profit,yoy
|
||||||
|
2020H1,1.85,25.2
|
||||||
|
2020H2,4.99,14.4
|
||||||
|
2021H1,4.58,147.3
|
||||||
|
2021H2,6.37,27.7
|
||||||
|
2022H1,3.54,-22.5
|
||||||
|
2022H2,1.43,-77.6
|
||||||
|
2023H1,4.89,37.9
|
||||||
|
2023H2,6.63,363.8
|
||||||
|
2024H1,11.31,131.5
|
||||||
|
2024H2,29.79,349.1
|
||||||
|
2025H1,57.98,412.4
|
||||||
|
2025H2,111.14,273.1
|
||||||
|
2026H1,71.08,22.6
|
||||||
|
@@ -1,7 +1,20 @@
|
|||||||
period,sales_expense,admin_expense,financial_expense,four_expense_rate
|
period,sales_expense,admin_expense,financial_expense,four_expense_rate
|
||||||
|
2020H1,2.23,1.25,0.04,43.1
|
||||||
|
2020H2,4.07,1.55,0.05,33.4
|
||||||
2020FY,6.30,2.80,0.09,36.6
|
2020FY,6.30,2.80,0.09,36.6
|
||||||
|
2021H1,4.20,2.40,-0.06,36.9
|
||||||
|
2021H2,6.86,3.18,-0.02,36.9
|
||||||
2021FY,11.06,5.58,-0.07,36.9
|
2021FY,11.06,5.58,-0.07,36.9
|
||||||
|
2022H1,6.93,3.23,-0.07,42.8
|
||||||
|
2022H2,7.78,3.64,-0.22,49.5
|
||||||
2022FY,14.71,6.86,-0.29,46.1
|
2022FY,14.71,6.86,-0.29,46.1
|
||||||
|
2023H1,8.78,3.31,-0.63,40.7
|
||||||
|
2023H2,11.26,3.76,-0.88,40.5
|
||||||
2023FY,20.05,7.07,-1.52,40.6
|
2023FY,20.05,7.07,-1.52,40.6
|
||||||
|
2024H1,13.53,4.34,-0.84,37.4
|
||||||
|
2024H2,22.97,5.13,-0.80,32.2
|
||||||
2024FY,36.50,9.47,-1.63,34.0
|
2024FY,36.50,9.47,-1.63,34.0
|
||||||
|
2025H1,31.93,7.70,-0.66,28.1
|
||||||
|
2025H2,48.90,10.00,-0.11,25.3
|
||||||
2025FY,80.82,17.70,-0.76,26.3
|
2025FY,80.82,17.70,-0.76,26.3
|
||||||
|
2026H1,39.31,9.27,-0.11,28.2
|
||||||
|
|||||||
|
@@ -0,0 +1,7 @@
|
|||||||
|
period,sales_expense,admin_expense,financial_expense,four_expense_rate
|
||||||
|
2020FY,6.30,2.80,0.09,36.6
|
||||||
|
2021FY,11.06,5.58,-0.07,36.9
|
||||||
|
2022FY,14.71,6.86,-0.29,46.1
|
||||||
|
2023FY,20.05,7.07,-1.52,40.6
|
||||||
|
2024FY,36.50,9.47,-1.63,34.0
|
||||||
|
2025FY,80.82,17.70,-0.76,26.3
|
||||||
|
@@ -0,0 +1,14 @@
|
|||||||
|
period,sales_expense,admin_expense,financial_expense,four_expense_rate
|
||||||
|
2020H1,2.23,1.25,0.04,43.1
|
||||||
|
2020H2,4.07,1.55,0.05,33.4
|
||||||
|
2021H1,4.20,2.40,-0.06,36.9
|
||||||
|
2021H2,6.86,3.18,-0.02,36.9
|
||||||
|
2022H1,6.93,3.23,-0.07,42.8
|
||||||
|
2022H2,7.78,3.64,-0.22,49.5
|
||||||
|
2023H1,8.78,3.31,-0.63,40.7
|
||||||
|
2023H2,11.26,3.76,-0.88,40.5
|
||||||
|
2024H1,13.53,4.34,-0.84,37.4
|
||||||
|
2024H2,22.97,5.13,-0.80,32.2
|
||||||
|
2025H1,31.93,7.70,-0.66,28.1
|
||||||
|
2025H2,48.90,10.00,-0.11,25.3
|
||||||
|
2026H1,39.31,9.27,-0.11,28.2
|
||||||
|
+17
-11
@@ -1,14 +1,20 @@
|
|||||||
period,non_gaap_net_profit,gross_profit,revenue,cost
|
period,non_gaap_net_profit,gross_profit,revenue,cost
|
||||||
2020H1,23.7,61.0,50.6,33.3
|
2020H1,24.4,61.4,50.5,33.6
|
||||||
2020H2,16.2,46.2,49.3,55.0
|
2020H2,33.1,39.6,48.7,67.0
|
||||||
2021H1,154.6,109.6,116.7,131.0
|
2020FY,30.9,46.2,49.3,55.0
|
||||||
2021H2,69.5,73.1,78.7,88.5
|
2021H1,208.0,109.4,116.8,130.5
|
||||||
2022H1,-13.6,22.6,33.1,50.6
|
2021H2,26.1,54.8,60.3,69.5
|
||||||
2022H2,-42.7,-3.8,2.8,13.3
|
2021FY,69.6,73.1,78.7,88.4
|
||||||
|
2022H1,-13.5,22.7,33.1,50.8
|
||||||
|
2022H2,-65.2,-21.8,-16.9,-9.5
|
||||||
|
2022FY,-42.7,-3.8,2.8,13.3
|
||||||
2023H1,42.3,23.9,19.3,12.9
|
2023H1,42.3,23.9,19.3,12.9
|
||||||
2023H2,107.5,45.5,36.5,24.1
|
2023H2,232.1,68.7,54.4,35.6
|
||||||
2024H1,90.3,71.9,62.0,47.0
|
2023FY,107.6,45.6,36.5,24.2
|
||||||
2024H2,185.7,125.4,106.9,77.7
|
2024H1,90.1,71.9,62.0,46.9
|
||||||
2025H1,362.7,234.4,204.4,151.1
|
2024H2,264.1,167.3,143.2,103.7
|
||||||
2025H2,284.5,207.4,184.7,139.1
|
2024FY,185.9,125.4,106.9,77.7
|
||||||
|
2025H1,362.8,234.4,204.4,151.1
|
||||||
|
2025H2,251.0,193.7,174.1,131.9
|
||||||
|
2025FY,284.5,207.4,184.7,139.1
|
||||||
2026H1,9.5,22.6,23.8,26.5
|
2026H1,9.5,22.6,23.8,26.5
|
||||||
|
|||||||
|
@@ -0,0 +1,7 @@
|
|||||||
|
period,non_gaap_net_profit,gross_profit,revenue,cost
|
||||||
|
2020FY,30.9,46.2,49.3,55.0
|
||||||
|
2021FY,69.6,73.1,78.7,88.4
|
||||||
|
2022FY,-42.7,-3.8,2.8,13.3
|
||||||
|
2023FY,107.6,45.6,36.5,24.2
|
||||||
|
2024FY,185.9,125.4,106.9,77.7
|
||||||
|
2025FY,284.5,207.4,184.7,139.1
|
||||||
|
@@ -0,0 +1,14 @@
|
|||||||
|
period,non_gaap_net_profit,gross_profit,revenue,cost
|
||||||
|
2020H1,24.4,61.4,50.5,33.6
|
||||||
|
2020H2,33.1,39.6,48.7,67.0
|
||||||
|
2021H1,208.0,109.4,116.8,130.5
|
||||||
|
2021H2,26.1,54.8,60.3,69.5
|
||||||
|
2022H1,-13.5,22.7,33.1,50.8
|
||||||
|
2022H2,-65.2,-21.8,-16.9,-9.5
|
||||||
|
2023H1,42.3,23.9,19.3,12.9
|
||||||
|
2023H2,232.1,68.7,54.4,35.6
|
||||||
|
2024H1,90.1,71.9,62.0,46.9
|
||||||
|
2024H2,264.1,167.3,143.2,103.7
|
||||||
|
2025H1,362.8,234.4,204.4,151.1
|
||||||
|
2025H2,251.0,193.7,174.1,131.9
|
||||||
|
2026H1,9.5,22.6,23.8,26.5
|
||||||
|
@@ -0,0 +1,17 @@
|
|||||||
|
period,inventory,yoy
|
||||||
|
2017FY,0.16,
|
||||||
|
2018FY,0.29,87.0
|
||||||
|
2019FY,0.96,231.4
|
||||||
|
2020H1,2.24,
|
||||||
|
2020FY,2.25,134.0
|
||||||
|
2021H1,3.16,41.0
|
||||||
|
2021FY,7.89,250.0
|
||||||
|
2022H1,9.57,203.0
|
||||||
|
2022FY,8.67,9.9
|
||||||
|
2023H1,7.59,-20.8
|
||||||
|
2023FY,9.05,4.4
|
||||||
|
2024H1,9.17,20.8
|
||||||
|
2024FY,15.25,68.5
|
||||||
|
2025H1,22.74,148.0
|
||||||
|
2025FY,54.73,259.0
|
||||||
|
2026H1,61.02,168.4
|
||||||
|
@@ -0,0 +1,10 @@
|
|||||||
|
period,inventory,yoy
|
||||||
|
2017FY,0.16,
|
||||||
|
2018FY,0.29,87.0
|
||||||
|
2019FY,0.96,231.4
|
||||||
|
2020FY,2.25,134.0
|
||||||
|
2021FY,7.89,250.0
|
||||||
|
2022FY,8.67,9.9
|
||||||
|
2023FY,9.05,4.4
|
||||||
|
2024FY,15.25,68.5
|
||||||
|
2025FY,54.73,259.0
|
||||||
|
@@ -0,0 +1,8 @@
|
|||||||
|
period,inventory,yoy
|
||||||
|
2020H1,2.24,
|
||||||
|
2021H1,3.16,41.0
|
||||||
|
2022H1,9.57,203.0
|
||||||
|
2023H1,7.59,-20.8
|
||||||
|
2024H1,9.17,20.8
|
||||||
|
2025H1,22.74,148.0
|
||||||
|
2026H1,61.02,168.4
|
||||||
|
@@ -1,9 +1,9 @@
|
|||||||
ip,2021H1,2021H2,2022H1,2022H2,2023H1,2023H2,2024H1,2024H2,2025H1,2025H2
|
ip,2021H1,2021H2,2021FY,2022H1,2022H2,2022FY,2023H1,2023H2,2023FY,2024H1,2024H2,2024FY,2025H1,2025H2,2025FY
|
||||||
THE MONSTERS,1.45789,1.58642,1.34887,1.28115,1.59829,2.08042,6.26806,24.13858,48.14017,93.47047
|
THE MONSTERS,1.45789,1.58642,3.04431,1.34887,1.28115,2.63002,1.59829,2.08042,3.67871,6.26806,24.13858,30.406640000000003,48.14017,93.47047,141.61064
|
||||||
MOLLY,2.039,5.01198,4.04347,3.97808,4.11433,6.08872,7.8216,13.11012,13.57248,15.39836
|
MOLLY,2.039,5.01198,7.050980000000001,4.04347,3.97808,8.02155,4.11433,6.08872,10.203050000000001,7.8216,13.11012,20.93172,13.57248,15.39836,28.970840000000003
|
||||||
SKULLPANDA,1.83042,4.12214,4.61766,3.89845,5.26383,4.98405,5.74562,7.337,12.20504,23.19038
|
SKULLPANDA,1.83042,4.12214,5.95256,4.61766,3.89845,8.51611,5.26383,4.98405,10.247879999999999,5.74562,7.337,13.082619999999999,12.20504,23.19038,35.39542
|
||||||
CRYBABY,0,0,0,0,0.26603,0.44546,3.49382,8.15503,12.1817,17.10836
|
CRYBABY,0,0,0,0,0,0,0.26603,0.44546,0.71149,3.49382,8.15503,11.64885,12.1817,17.10836,29.29006
|
||||||
DIMOO,2.04993,3.61742,2.98442,2.79506,3.62032,3.75634,3.77838,5.31723,11.05135,16.7156
|
DIMOO,2.04993,3.61742,5.66735,2.98442,2.79506,5.7794799999999995,3.62032,3.75634,7.376659999999999,3.77838,5.31723,9.09561,11.05135,16.7156,27.766949999999998
|
||||||
HIRONO-小野,0,0,0.37538,1.0323,1.09352,2.41849,2.45247,4.81278,7.2842,10.06815
|
HIRONO-小野,0,0,0,0.37538,1.0323,1.40768,1.09352,2.41849,3.51201,2.45247,4.81278,7.26525,7.2842,10.06815,17.35235
|
||||||
HACIPUPU,0,0,0,0.42797,0.69171,0.82587,0.94991,1.29297,3.32122,5.15899
|
HACIPUPU,0,0,0,0,0.42797,0.42797,0.69171,0.82587,1.5175800000000002,0.94991,1.29297,2.24288,3.32122,5.15899,8.48021
|
||||||
星星人,0,0,0,0,0,0,0,0,3.89363,16.66217
|
星星人,0,0,0,0,0,0,0,0,0,0,0,0,3.89363,16.66217,20.555799999999998
|
||||||
|
|||||||
|
@@ -0,0 +1,9 @@
|
|||||||
|
ip,2021FY,2022FY,2023FY,2024FY,2025FY
|
||||||
|
THE MONSTERS,3.04431,2.63002,3.67871,30.406640000000003,141.61064
|
||||||
|
MOLLY,7.050980000000001,8.02155,10.203050000000001,20.93172,28.970840000000003
|
||||||
|
SKULLPANDA,5.95256,8.51611,10.247879999999999,13.082619999999999,35.39542
|
||||||
|
CRYBABY,0,0,0.71149,11.64885,29.29006
|
||||||
|
DIMOO,5.66735,5.7794799999999995,7.376659999999999,9.09561,27.766949999999998
|
||||||
|
HIRONO-小野,0,1.40768,3.51201,7.26525,17.35235
|
||||||
|
HACIPUPU,0,0.42797,1.5175800000000002,2.24288,8.48021
|
||||||
|
星星人,0,0,0,0,20.555799999999998
|
||||||
|
@@ -1,12 +1,17 @@
|
|||||||
period,revenue,yoy
|
period,revenue,yoy
|
||||||
2021H1,16.27,
|
2021H1,16.27,
|
||||||
2021H2,25.59,
|
2021H2,25.59,
|
||||||
|
2021FY,41.86,
|
||||||
2022H1,22.24,36.7
|
2022H1,22.24,36.7
|
||||||
2022H2,21.30,-16.8
|
2022H2,21.30,-16.8
|
||||||
|
2022FY,43.54,4.0
|
||||||
2023H1,26.54,19.3
|
2023H1,26.54,19.3
|
||||||
2023H2,32.79,53.9
|
2023H2,32.79,53.9
|
||||||
|
2023FY,59.33,36.3
|
||||||
2024H1,39.31,48.1
|
2024H1,39.31,48.1
|
||||||
2024H2,60.66,85.0
|
2024H2,60.66,85.0
|
||||||
|
2024FY,99.97,68.5
|
||||||
2025H1,90.62,130.5
|
2025H1,90.62,130.5
|
||||||
2025H2,138.97,129.1
|
2025H2,138.97,129.1
|
||||||
|
2025FY,229.59,129.7
|
||||||
2026H1,127.19,40.4
|
2026H1,127.19,40.4
|
||||||
|
|||||||
|
@@ -0,0 +1,6 @@
|
|||||||
|
period,revenue,yoy
|
||||||
|
2021FY,41.86,
|
||||||
|
2022FY,43.54,4.0
|
||||||
|
2023FY,59.33,36.3
|
||||||
|
2024FY,99.97,68.5
|
||||||
|
2025FY,229.59,129.7
|
||||||
|
@@ -0,0 +1,12 @@
|
|||||||
|
period,revenue,yoy
|
||||||
|
2021H1,16.27,
|
||||||
|
2021H2,25.59,
|
||||||
|
2022H1,22.24,36.7
|
||||||
|
2022H2,21.30,-16.8
|
||||||
|
2023H1,26.54,19.3
|
||||||
|
2023H2,32.79,53.9
|
||||||
|
2024H1,39.31,48.1
|
||||||
|
2024H2,60.66,85.0
|
||||||
|
2025H1,90.62,130.5
|
||||||
|
2025H2,138.97,129.1
|
||||||
|
2026H1,127.19,40.4
|
||||||
|
@@ -0,0 +1,9 @@
|
|||||||
|
ip,2021H1,2021H2,2022H1,2022H2,2023H1,2023H2,2024H1,2024H2,2025H1,2025H2
|
||||||
|
THE MONSTERS,1.45789,1.58642,1.34887,1.28115,1.59829,2.08042,6.26806,24.13858,48.14017,93.47047
|
||||||
|
MOLLY,2.039,5.01198,4.04347,3.97808,4.11433,6.08872,7.8216,13.11012,13.57248,15.39836
|
||||||
|
SKULLPANDA,1.83042,4.12214,4.61766,3.89845,5.26383,4.98405,5.74562,7.337,12.20504,23.19038
|
||||||
|
CRYBABY,0,0,0,0,0.26603,0.44546,3.49382,8.15503,12.1817,17.10836
|
||||||
|
DIMOO,2.04993,3.61742,2.98442,2.79506,3.62032,3.75634,3.77838,5.31723,11.05135,16.7156
|
||||||
|
HIRONO-小野,0,0,0.37538,1.0323,1.09352,2.41849,2.45247,4.81278,7.2842,10.06815
|
||||||
|
HACIPUPU,0,0,0,0.42797,0.69171,0.82587,0.94991,1.29297,3.32122,5.15899
|
||||||
|
星星人,0,0,0,0,0,0,0,0,3.89363,16.66217
|
||||||
|
@@ -1,12 +1,12 @@
|
|||||||
ip,2021H1,2021H2,2022H1,2022H2,2023H1,2023H2,2024H1,2024H2,2025H1,2025H2,2026H1
|
ip,2021H1,2021H2,2021FY,2022H1,2022H2,2022FY,2023H1,2023H2,2023FY,2024H1,2024H2,2024FY,2025H1,2025H2,2025FY,2026H1
|
||||||
THE MONSTERS,1.46,1.59,1.35,1.28,1.60,2.08,6.27,24.14,48.14,93.47,44.54
|
THE MONSTERS,1.46,1.59,3.05,1.35,1.28,2.63,1.6,2.08,3.68,6.27,24.14,30.41,48.14,93.47,141.61,44.54
|
||||||
星星人,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,3.89,16.66,26.50
|
星星人,0,0,0,0,0,0,0,0,0,0,0,0,3.89,16.66,20.55,26.5
|
||||||
授权IP(非独家),2.88,3.33,3.01,2.50,4.22,6.14,6.95,9.06,15.25,18.57,17.71
|
授权IP(非独家),2.88,3.33,6.21,3.01,2.5,5.51,4.22,6.14,10.36,6.95,9.06,16.01,15.25,18.57,33.82,17.71
|
||||||
CRYBABY,0.00,0.00,0.00,0.00,0.27,0.45,3.49,8.16,12.18,17.11,16.33
|
CRYBABY,0,0,0,0,0,0,0.27,0.45,0.72,3.49,8.16,11.65,12.18,17.11,29.29,16.33
|
||||||
DIMOO,2.05,3.62,2.98,2.80,3.62,3.76,3.78,5.32,11.05,16.72,16.19
|
DIMOO,2.05,3.62,5.67,2.98,2.8,5.779999999999999,3.62,3.76,7.38,3.78,5.32,9.1,11.05,16.72,27.77,16.19
|
||||||
SKULLPANDA,1.83,4.12,4.62,3.90,5.26,4.98,5.75,7.34,12.21,23.19,15.51
|
SKULLPANDA,1.83,4.12,5.95,4.62,3.9,8.52,5.26,4.98,10.24,5.75,7.34,13.09,12.21,23.19,35.400000000000006,15.51
|
||||||
其他艺术家IP,1.43,2.47,4.37,3.00,4.54,5.44,6.37,7.72,10.64,14.00,11.74
|
其他艺术家IP,1.43,2.47,3.9000000000000004,4.37,3,7.37,4.54,5.44,9.98,6.37,7.72,14.09,10.64,14,24.64,11.74
|
||||||
HIRONO-小野,0.00,0.00,0.38,1.03,1.09,2.42,2.45,4.81,7.28,10.07,10.09
|
HIRONO-小野,0,0,0,0.38,1.03,1.4100000000000001,1.09,2.42,3.51,2.45,4.81,7.26,7.28,10.07,17.35,10.09
|
||||||
MOLLY,2.04,5.01,4.04,3.98,4.11,6.09,7.82,13.11,13.57,15.40,9.01
|
MOLLY,2.04,5.01,7.05,4.04,3.98,8.02,4.11,6.09,10.2,7.82,13.11,20.93,13.57,15.4,28.97,9.01
|
||||||
其他IP(含已停单列),4.14,4.51,0.81,1.86,1.15,1.37,0.95,3.74,3.32,5.16,2.98
|
其他IP(含已停单列),4.14,4.51,8.649999999999999,0.81,1.86,2.67,1.15,1.37,2.52,0.95,3.74,4.69,3.32,5.16,8.48,2.98
|
||||||
外采及其他,1.90,2.53,2.03,2.24,2.28,2.15,1.75,1.41,1.23,2.09,1.12
|
外采及其他,1.9,2.53,4.43,2.03,2.24,4.27,2.28,2.15,4.43,1.75,1.41,3.16,1.23,2.09,3.32,1.12
|
||||||
|
|||||||
|
@@ -0,0 +1,12 @@
|
|||||||
|
ip,2021FY,2022FY,2023FY,2024FY,2025FY
|
||||||
|
THE MONSTERS,3.05,2.63,3.68,30.41,141.61
|
||||||
|
星星人,0,0,0,0,20.55
|
||||||
|
授权IP(非独家),6.21,5.51,10.36,16.01,33.82
|
||||||
|
CRYBABY,0,0,0.72,11.65,29.29
|
||||||
|
DIMOO,5.67,5.779999999999999,7.38,9.1,27.77
|
||||||
|
SKULLPANDA,5.95,8.52,10.24,13.09,35.400000000000006
|
||||||
|
其他艺术家IP,3.9000000000000004,7.37,9.98,14.09,24.64
|
||||||
|
HIRONO-小野,0,1.4100000000000001,3.51,7.26,17.35
|
||||||
|
MOLLY,7.05,8.02,10.2,20.93,28.97
|
||||||
|
其他IP(含已停单列),8.649999999999999,2.67,2.52,4.69,8.48
|
||||||
|
外采及其他,4.43,4.27,4.43,3.16,3.32
|
||||||
|
@@ -0,0 +1,12 @@
|
|||||||
|
ip,2021H1,2021H2,2022H1,2022H2,2023H1,2023H2,2024H1,2024H2,2025H1,2025H2,2026H1
|
||||||
|
THE MONSTERS,1.46,1.59,1.35,1.28,1.6,2.08,6.27,24.14,48.14,93.47,44.54
|
||||||
|
星星人,0,0,0,0,0,0,0,0,3.89,16.66,26.5
|
||||||
|
授权IP(非独家),2.88,3.33,3.01,2.5,4.22,6.14,6.95,9.06,15.25,18.57,17.71
|
||||||
|
CRYBABY,0,0,0,0,0.27,0.45,3.49,8.16,12.18,17.11,16.33
|
||||||
|
DIMOO,2.05,3.62,2.98,2.8,3.62,3.76,3.78,5.32,11.05,16.72,16.19
|
||||||
|
SKULLPANDA,1.83,4.12,4.62,3.9,5.26,4.98,5.75,7.34,12.21,23.19,15.51
|
||||||
|
其他艺术家IP,1.43,2.47,4.37,3,4.54,5.44,6.37,7.72,10.64,14,11.74
|
||||||
|
HIRONO-小野,0,0,0.38,1.03,1.09,2.42,2.45,4.81,7.28,10.07,10.09
|
||||||
|
MOLLY,2.04,5.01,4.04,3.98,4.11,6.09,7.82,13.11,13.57,15.4,9.01
|
||||||
|
其他IP(含已停单列),4.14,4.51,0.81,1.86,1.15,1.37,0.95,3.74,3.32,5.16,2.98
|
||||||
|
外采及其他,1.9,2.53,2.03,2.24,2.28,2.15,1.75,1.41,1.23,2.09,1.12
|
||||||
|
@@ -1,14 +1,14 @@
|
|||||||
period,cash,total_liabilities,net_working_capital
|
period,cash,total_liabilities,net_working_capital
|
||||||
2020H1,8.22,5.47,8.66
|
2020H1,8.22,5.47,8.66
|
||||||
2020FY,56.80,8.40,55.91
|
2020H2,56.80,8.40,55.91
|
||||||
2021H1,57.67,13.26,55.92
|
2021H1,57.67,13.26,55.92
|
||||||
2021FY,52.65,15.04,56.65
|
2021H2,52.65,15.04,56.65
|
||||||
2022H1,51.63,14.76,58.16
|
2022H1,51.63,14.76,58.16
|
||||||
2022FY,50.42,16.15,54.85
|
2022H2,50.42,16.15,54.85
|
||||||
2023H1,55.40,17.59,57.33
|
2023H1,55.40,17.59,57.33
|
||||||
2023FY,59.63,21.88,59.50
|
2023H2,59.63,21.88,59.50
|
||||||
2024H1,70.10,25.65,66.47
|
2024H1,70.10,25.65,66.47
|
||||||
2024FY,96.20,39.86,88.66
|
2024H2,96.20,39.86,88.66
|
||||||
2025H1,137.66,69.27,119.27
|
2025H1,137.66,69.27,119.27
|
||||||
2025FY,172.25,94.49,177.46
|
2025H2,172.25,94.49,177.46
|
||||||
2026H1,139.62,77.40,174.20
|
2026H1,139.62,77.40,174.20
|
||||||
|
|||||||
|
@@ -0,0 +1,7 @@
|
|||||||
|
period,cash,total_liabilities,net_working_capital
|
||||||
|
2020FY,56.80,8.40,55.91
|
||||||
|
2021FY,52.65,15.04,56.65
|
||||||
|
2022FY,50.42,16.15,54.85
|
||||||
|
2023FY,59.63,21.88,59.50
|
||||||
|
2024FY,96.20,39.86,88.66
|
||||||
|
2025FY,172.25,94.49,177.46
|
||||||
|
@@ -0,0 +1,14 @@
|
|||||||
|
period,cash,total_liabilities,net_working_capital
|
||||||
|
2020H1,8.22,5.47,8.66
|
||||||
|
2020H2,56.80,8.40,55.91
|
||||||
|
2021H1,57.67,13.26,55.92
|
||||||
|
2021H2,52.65,15.04,56.65
|
||||||
|
2022H1,51.63,14.76,58.16
|
||||||
|
2022H2,50.42,16.15,54.85
|
||||||
|
2023H1,55.40,17.59,57.33
|
||||||
|
2023H2,59.63,21.88,59.50
|
||||||
|
2024H1,70.10,25.65,66.47
|
||||||
|
2024H2,96.20,39.86,88.66
|
||||||
|
2025H1,137.66,69.27,119.27
|
||||||
|
2025H2,172.25,94.49,177.46
|
||||||
|
2026H1,139.62,77.40,174.20
|
||||||
|
+17
-11
@@ -1,14 +1,20 @@
|
|||||||
period,gross_margin,sales_expense_rate,admin_expense_rate,non_gaap_net_margin
|
period,gross_margin,sales_expense_rate,admin_expense_rate,non_gaap_net_margin
|
||||||
2020H1,65.23,27.27,15.33,17.24
|
2020H1,65.23,27.27,15.33,17.28
|
||||||
2020H2,63.42,25.07,11.14,23.52
|
2020H2,62.55,24.00,9.12,26.49
|
||||||
2021H1,63.02,23.68,13.52,24.53
|
2020FY,63.42,25.07,11.14,23.49
|
||||||
2021H2,61.43,24.63,12.41,22.31
|
2021H1,63.02,23.68,13.52,24.55
|
||||||
2022H1,58.1,29.39,13.68,15.94
|
2021H2,60.39,25.25,11.69,20.84
|
||||||
2022H2,57.49,31.85,14.86,12.43
|
2021FY,61.43,24.63,12.41,22.30
|
||||||
2023H1,60.36,31.21,11.77,19.01
|
2022H1,58.10,29.39,13.68,15.95
|
||||||
2023H2,61.32,31.82,11.23,18.9
|
2022H2,56.86,34.43,16.10,8.73
|
||||||
|
2022FY,57.49,31.85,14.86,12.42
|
||||||
|
2023H1,60.36,31.21,11.77,19.03
|
||||||
|
2023H2,62.10,32.30,10.78,18.79
|
||||||
|
2023FY,61.32,31.82,11.23,18.89
|
||||||
2024H1,64.05,29.69,9.53,22.33
|
2024H1,64.05,29.69,9.53,22.33
|
||||||
2024H2,66.79,28.0,7.26,26.1
|
2024H2,68.26,27.09,6.05,28.13
|
||||||
|
2024FY,66.79,28.00,7.26,26.10
|
||||||
2025H1,70.34,23.01,5.55,33.94
|
2025H1,70.34,23.01,5.55,33.94
|
||||||
2025H2,72.1,21.77,4.77,35.25
|
2025H2,73.15,21.04,4.30,36.03
|
||||||
2026H1,69.68,22.89,5.4,30.02
|
2025FY,72.10,21.77,4.77,35.25
|
||||||
|
2026H1,69.68,22.89,5.40,30.02
|
||||||
|
|||||||
|
@@ -0,0 +1,7 @@
|
|||||||
|
period,gross_margin,sales_expense_rate,admin_expense_rate,non_gaap_net_margin
|
||||||
|
2020FY,63.42,25.07,11.14,23.49
|
||||||
|
2021FY,61.43,24.63,12.41,22.30
|
||||||
|
2022FY,57.49,31.85,14.86,12.42
|
||||||
|
2023FY,61.32,31.82,11.23,18.89
|
||||||
|
2024FY,66.79,28.00,7.26,26.10
|
||||||
|
2025FY,72.10,21.77,4.77,35.25
|
||||||
|
@@ -0,0 +1,14 @@
|
|||||||
|
period,gross_margin,sales_expense_rate,admin_expense_rate,non_gaap_net_margin
|
||||||
|
2020H1,65.23,27.27,15.33,17.28
|
||||||
|
2020H2,62.55,24.00,9.12,26.49
|
||||||
|
2021H1,63.02,23.68,13.52,24.55
|
||||||
|
2021H2,60.39,25.25,11.69,20.84
|
||||||
|
2022H1,58.10,29.39,13.68,15.95
|
||||||
|
2022H2,56.86,34.43,16.10,8.73
|
||||||
|
2023H1,60.36,31.21,11.77,19.03
|
||||||
|
2023H2,62.10,32.30,10.78,18.79
|
||||||
|
2024H1,64.05,29.69,9.53,22.33
|
||||||
|
2024H2,68.26,27.09,6.05,28.13
|
||||||
|
2025H1,70.34,23.01,5.55,33.94
|
||||||
|
2025H2,73.15,21.04,4.30,36.03
|
||||||
|
2026H1,69.68,22.89,5.40,30.02
|
||||||
|
+18
-16
@@ -1,18 +1,20 @@
|
|||||||
period,revenue,gross_profit,gross_margin,net_profit
|
period,revenue,gross_profit,gross_margin,net_profit
|
||||||
2026H1,171.73,119.66,69.67999999999999,50.38
|
|
||||||
2025H2,371.2,267.65,72.1,127.76
|
|
||||||
2025H1,138.76,97.61,70.34,45.74
|
|
||||||
2024H2,130.38,87.08,66.79,31.25
|
|
||||||
2024H1,45.58,29.19,64.05,9.21
|
|
||||||
2023H2,63.01,38.64,61.31999999999999,10.82
|
|
||||||
2023H1,28.14,16.98,60.36,4.77
|
|
||||||
2022H2,46.17,26.55,57.489999999999995,4.76
|
|
||||||
2022H1,23.59,13.7,58.099999999999994,3.33
|
|
||||||
2021H2,44.91,27.59,61.42999999999999,8.54
|
|
||||||
2021H1,17.73,11.17,63.019999999999996,3.59
|
|
||||||
2020H2,25.13,15.94,63.42,5.24
|
|
||||||
2020H1,8.18,5.33,65.23,1.41
|
2020H1,8.18,5.33,65.23,1.41
|
||||||
2019H2,16.83,10.9,64.77000000000001,4.51
|
2020H2,16.96,10.61,62.55,3.82
|
||||||
2019H1,5.43,3.31,60.84,1.14
|
2020FY,25.13,15.94,63.42,5.23
|
||||||
2018H2,5.15,2.98,57.92,1
|
2021H1,17.73,11.17,63.02,3.59
|
||||||
2017H2,1.58,0.75,47.61,0.02
|
2021H2,27.18,16.42,60.39,4.96
|
||||||
|
2021FY,44.91,27.59,61.43,8.55
|
||||||
|
2022H1,23.59,13.70,58.10,3.33
|
||||||
|
2022H2,22.59,12.84,56.86,1.43
|
||||||
|
2022FY,46.17,26.55,57.49,4.76
|
||||||
|
2023H1,28.14,16.98,60.36,4.77
|
||||||
|
2023H2,34.87,21.66,62.10,6.12
|
||||||
|
2023FY,63.01,38.64,61.32,10.89
|
||||||
|
2024H1,45.58,29.19,64.05,9.64
|
||||||
|
2024H2,84.80,57.89,68.26,23.44
|
||||||
|
2024FY,130.38,87.08,66.79,33.08
|
||||||
|
2025H1,138.76,97.61,70.34,46.82
|
||||||
|
2025H2,232.44,170.04,73.15,83.30
|
||||||
|
2025FY,371.20,267.65,72.10,130.12
|
||||||
|
2026H1,171.73,119.66,69.68,51.00
|
||||||
|
|||||||
|
@@ -0,0 +1,7 @@
|
|||||||
|
period,revenue,gross_profit,gross_margin,net_profit
|
||||||
|
2020FY,25.13,15.94,63.42,5.23
|
||||||
|
2021FY,44.91,27.59,61.43,8.55
|
||||||
|
2022FY,46.17,26.55,57.49,4.76
|
||||||
|
2023FY,63.01,38.64,61.32,10.89
|
||||||
|
2024FY,130.38,87.08,66.79,33.08
|
||||||
|
2025FY,371.20,267.65,72.10,130.12
|
||||||
|
@@ -0,0 +1,14 @@
|
|||||||
|
period,revenue,gross_profit,gross_margin,net_profit
|
||||||
|
2020H1,8.18,5.33,65.23,1.41
|
||||||
|
2020H2,16.96,10.61,62.55,3.82
|
||||||
|
2021H1,17.73,11.17,63.02,3.59
|
||||||
|
2021H2,27.18,16.42,60.39,4.96
|
||||||
|
2022H1,23.59,13.70,58.10,3.33
|
||||||
|
2022H2,22.59,12.84,56.86,1.43
|
||||||
|
2023H1,28.14,16.98,60.36,4.77
|
||||||
|
2023H2,34.87,21.66,62.10,6.12
|
||||||
|
2024H1,45.58,29.19,64.05,9.64
|
||||||
|
2024H2,84.80,57.89,68.26,23.44
|
||||||
|
2025H1,138.76,97.61,70.34,46.82
|
||||||
|
2025H2,232.44,170.04,73.15,83.30
|
||||||
|
2026H1,171.73,119.66,69.68,51.00
|
||||||
|
@@ -0,0 +1,8 @@
|
|||||||
|
period,china,apac,americas,europe,total
|
||||||
|
2024H1,35.22,7.97,1.82,0.58,45.59
|
||||||
|
2024H2,53.65,23.13,6.20,1.81,84.79
|
||||||
|
2024FY,88.86,31.10,8.02,2.39,130.37
|
||||||
|
2025H1,82.83,28.51,22.65,4.77,138.76
|
||||||
|
2025H2,125.69,51.60,45.41,9.74,232.44
|
||||||
|
2025FY,208.52,80.11,68.06,14.51,371.20
|
||||||
|
2026H1,122.01,25.75,18.92,5.06,171.74
|
||||||
|
@@ -0,0 +1,3 @@
|
|||||||
|
period,china,apac,americas,europe,total
|
||||||
|
2024FY,88.86,31.10,8.02,2.39,130.37
|
||||||
|
2025FY,208.52,80.11,68.06,14.51,371.20
|
||||||
|
@@ -0,0 +1,6 @@
|
|||||||
|
period,china,apac,americas,europe,total
|
||||||
|
2024H1,35.22,7.97,1.82,0.58,45.59
|
||||||
|
2024H2,53.65,23.13,6.20,1.81,84.79
|
||||||
|
2025H1,82.83,28.51,22.65,4.77,138.76
|
||||||
|
2025H2,125.69,51.60,45.41,9.74,232.44
|
||||||
|
2026H1,122.01,25.75,18.92,5.06,171.74
|
||||||
|
@@ -0,0 +1,8 @@
|
|||||||
|
period,china_retail_revenue,china_average_stores,china_per_store,apac_retail_revenue,apac_average_stores,apac_per_store,americas_retail_revenue,americas_average_stores,americas_per_store,europe_other_retail_revenue,europe_other_average_stores,europe_other_per_store
|
||||||
|
2024H1,20.04,381.0,526.0,,,,,,,,,
|
||||||
|
2024H2,25.97,415.0,625.8,11.83,46.5,2544.1,2.54,16.0,1587.5,1.16,11.5,1008.7
|
||||||
|
2024FY,46.01,397.0,1158.9,,,,,,,,,
|
||||||
|
2025H1,44.06,437.0,1008.2,14.93,61.5,2427.6,7.37,31.5,2339.7,2.72,16.0,1700.0
|
||||||
|
2025H2,56.69,444.0,1276.8,28.26,77.0,3670.1,12.67,52.5,2413.3,5.84,27.0,2163.0
|
||||||
|
2025FY,100.75,438.0,2300.2,43.19,69.5,6214.4,20.04,43.0,4660.5,8.56,25.0,3424.0
|
||||||
|
2026H1,60.90,450.0,1353.3,17.16,87.5,1961.1,9.03,75.0,1204.0,4.03,40.5,995.1
|
||||||
|
@@ -0,0 +1,3 @@
|
|||||||
|
period,china_retail_revenue,china_average_stores,china_per_store,apac_retail_revenue,apac_average_stores,apac_per_store,americas_retail_revenue,americas_average_stores,americas_per_store,europe_other_retail_revenue,europe_other_average_stores,europe_other_per_store
|
||||||
|
2024FY,46.01,397.0,1158.9,,,,,,,,,
|
||||||
|
2025FY,100.75,438.0,2300.2,43.19,69.5,6214.4,20.04,43.0,4660.5,8.56,25.0,3424.0
|
||||||
|
@@ -0,0 +1,6 @@
|
|||||||
|
period,china_retail_revenue,china_average_stores,china_per_store,apac_retail_revenue,apac_average_stores,apac_per_store,americas_retail_revenue,americas_average_stores,americas_per_store,europe_other_retail_revenue,europe_other_average_stores,europe_other_per_store
|
||||||
|
2024H1,20.04,381.0,526.0,,,,,,,,,
|
||||||
|
2024H2,25.97,415.0,625.8,11.83,46.5,2544.1,2.54,16.0,1587.5,1.16,11.5,1008.7
|
||||||
|
2025H1,44.06,437.0,1008.2,14.93,61.5,2427.6,7.37,31.5,2339.7,2.72,16.0,1700.0
|
||||||
|
2025H2,56.69,444.0,1276.8,28.26,77.0,3670.1,12.67,52.5,2413.3,5.84,27.0,2163.0
|
||||||
|
2026H1,60.90,450.0,1353.3,17.16,87.5,1961.1,9.03,75.0,1204.0,4.03,40.5,995.1
|
||||||
|
@@ -0,0 +1,8 @@
|
|||||||
|
period,china_robot_revenue,china_average_stores,china_per_store,overseas_robot_revenue,overseas_average_stores,overseas_per_store
|
||||||
|
2024H1,3.38,2207.0,15.3,,,,
|
||||||
|
2024H2,4.13,2281.5,18.1,0.49,,
|
||||||
|
2024FY,7.51,2264.5,33.2,0.80,116.0,69.0
|
||||||
|
2025H1,6.78,2388.0,28.4,1.42,146.5,96.9
|
||||||
|
2025H2,6.68,2416.5,27.6,2.02,200.5,100.7
|
||||||
|
2025FY,13.46,2367.5,56.9,3.44,187.0,184.0
|
||||||
|
2026H1,7.79,2439.0,31.9,1.72,293.0,58.7
|
||||||
|
@@ -0,0 +1,3 @@
|
|||||||
|
period,china_robot_revenue,china_average_stores,china_per_store,overseas_robot_revenue,overseas_average_stores,overseas_per_store
|
||||||
|
2024FY,7.51,2264.5,33.2,0.80,116.0,69.0
|
||||||
|
2025FY,13.46,2367.5,56.9,3.44,187.0,184.0
|
||||||
|
@@ -0,0 +1,6 @@
|
|||||||
|
period,china_robot_revenue,china_average_stores,china_per_store,overseas_robot_revenue,overseas_average_stores,overseas_per_store
|
||||||
|
2024H1,3.38,2207.0,15.3,,,,
|
||||||
|
2024H2,4.13,2281.5,18.1,0.49,,
|
||||||
|
2025H1,6.78,2388.0,28.4,1.42,146.5,96.9
|
||||||
|
2025H2,6.68,2416.5,27.6,2.02,200.5,100.7
|
||||||
|
2026H1,7.79,2439.0,31.9,1.72,293.0,58.7
|
||||||
|
@@ -0,0 +1,13 @@
|
|||||||
|
period,mainland,hkmtw,apac,americas,europe,overseas,global,global_yoy
|
||||||
|
2017FY,32,,,,,,32,
|
||||||
|
2018FY,63,,,,,,63,96.9
|
||||||
|
2019FY,114,,,,,,114,81.0
|
||||||
|
2020FY,187,,,,,,187,64.0
|
||||||
|
2021FY,288,,,,,7,295,57.8
|
||||||
|
2022FY,329,,,,,43,372,26.1
|
||||||
|
2023FY,363,,,,,70,433,16.4
|
||||||
|
2024H1,374,25,39,10,9,58,457,
|
||||||
|
2024FY,401,30,54,22,14,90,521,20.3
|
||||||
|
2025H1,409,34,69,41,18,128,571,24.9
|
||||||
|
2025FY,410,35,85,64,36,185,630,20.9
|
||||||
|
2026H1,419,36,90,86,45,221,676,18.4
|
||||||
|
@@ -0,0 +1,10 @@
|
|||||||
|
period,mainland,hkmtw,apac,americas,europe,overseas,global,global_yoy
|
||||||
|
2017FY,32,,,,,,32,
|
||||||
|
2018FY,63,,,,,,63,96.9
|
||||||
|
2019FY,114,,,,,,114,81.0
|
||||||
|
2020FY,187,,,,,,187,64.0
|
||||||
|
2021FY,288,,,,,7,295,57.8
|
||||||
|
2022FY,329,,,,,43,372,26.1
|
||||||
|
2023FY,363,,,,,70,433,16.4
|
||||||
|
2024FY,401,30,54,22,14,90,521,20.3
|
||||||
|
2025FY,410,35,85,64,36,185,630,20.9
|
||||||
|
@@ -0,0 +1,4 @@
|
|||||||
|
period,mainland,hkmtw,apac,americas,europe,overseas,global,global_yoy
|
||||||
|
2024H1,374,25,39,10,9,58,457,
|
||||||
|
2025H1,409,34,69,41,18,128,571,24.9
|
||||||
|
2026H1,419,36,90,86,45,221,676,18.4
|
||||||
|
@@ -0,0 +1,6 @@
|
|||||||
|
period,global,china,apac,americas,europe,other
|
||||||
|
2024H1,457,87.3,8.5,2.2,2.0,0.0
|
||||||
|
2024FY,521,82.7,10.4,4.2,2.7,0.0
|
||||||
|
2025H1,571,77.6,12.1,7.2,3.2,0.0
|
||||||
|
2025FY,630,70.6,13.5,10.2,5.7,0.0
|
||||||
|
2026H1,676,67.3,13.3,12.7,6.7,0.0
|
||||||
|
@@ -0,0 +1,3 @@
|
|||||||
|
period,global,china,apac,americas,europe,other
|
||||||
|
2024FY,521,82.7,10.4,4.2,2.7,0.0
|
||||||
|
2025FY,630,70.6,13.5,10.2,5.7,0.0
|
||||||
|
@@ -0,0 +1,4 @@
|
|||||||
|
period,global,china,apac,americas,europe,other
|
||||||
|
2024H1,457,87.3,8.5,2.2,2.0,0.0
|
||||||
|
2025H1,571,77.6,12.1,7.2,3.2,0.0
|
||||||
|
2026H1,676,67.3,13.3,12.7,6.7,0.0
|
||||||
|
@@ -0,0 +1,13 @@
|
|||||||
|
period,mainland,hkmtw,overseas,global,global_yoy
|
||||||
|
2017FY,43,,,43,
|
||||||
|
2018FY,260,,,260,504.7
|
||||||
|
2019FY,825,,,825,217.3
|
||||||
|
2020FY,1351,,,1351,63.8
|
||||||
|
2021FY,1861,,9,1870,38.4
|
||||||
|
2022FY,2067,,120,2187,17.0
|
||||||
|
2023FY,2190,,99,2289,4.7
|
||||||
|
2024H1,2189,35,,,
|
||||||
|
2024FY,2300,39,133,2472,8.0
|
||||||
|
2025H1,2396,41,160,2597,
|
||||||
|
2025FY,2350,46,241,2637,6.7
|
||||||
|
2026H1,2436,46,345,2827,8.9
|
||||||
|
@@ -0,0 +1,10 @@
|
|||||||
|
period,mainland,hkmtw,overseas,global,global_yoy
|
||||||
|
2017FY,43,,,43,
|
||||||
|
2018FY,260,,,260,504.7
|
||||||
|
2019FY,825,,,825,217.3
|
||||||
|
2020FY,1351,,,1351,63.8
|
||||||
|
2021FY,1861,,9,1870,38.4
|
||||||
|
2022FY,2067,,120,2187,17.0
|
||||||
|
2023FY,2190,,99,2289,4.7
|
||||||
|
2024FY,2300,39,133,2472,8.0
|
||||||
|
2025FY,2350,46,241,2637,6.7
|
||||||
|
@@ -0,0 +1,4 @@
|
|||||||
|
period,mainland,hkmtw,overseas,global,global_yoy
|
||||||
|
2024H1,2189,35,,,
|
||||||
|
2025H1,2396,41,160,2597,
|
||||||
|
2026H1,2436,46,345,2827,8.9
|
||||||
|
@@ -1,14 +1,14 @@
|
|||||||
period,inventory_days,receivable_days,payable_days,cash_conversion_cycle
|
period,inventory_days,receivable_days,payable_days,cash_conversion_cycle
|
||||||
2020H1,101.4,9.6,27.6,83.3
|
2020H1,102.0,9.6,27.8,83.8
|
||||||
2020FY,63.0,8.9,28.4,43.5
|
2020FY,63.9,9.0,28.8,44.1
|
||||||
2021H1,74.3,10.9,40.9,44.3
|
2021H1,74.7,10.9,41.1,44.6
|
||||||
2021FY,105.4,10.0,29.9,85.5
|
2021FY,106.9,10.1,30.4,86.6
|
||||||
2022H1,159.0,11.6,44.7,125.9
|
2022H1,159.9,11.7,44.9,126.6
|
||||||
2022FY,151.8,14.3,46.3,119.8
|
2022FY,154.0,14.5,47.0,121.5
|
||||||
2023H1,131.2,12.9,54.4,89.7
|
2023H1,131.9,13.0,54.7,90.2
|
||||||
2023FY,130.9,14.7,51.2,94.4
|
2023FY,132.7,14.9,51.9,95.7
|
||||||
2024H1,100.0,11.6,54.5,57.1
|
2024H1,100.6,11.6,54.8,57.4
|
||||||
2024FY,101.0,11.0,52.9,59.1
|
2024FY,102.4,11.2,53.6,59.9
|
||||||
2025H1,83.1,9.4,48.8,43.7
|
2025H1,83.5,9.5,49.1,43.9
|
||||||
2025FY,121.6,6.8,36.1,92.3
|
2025FY,123.3,6.9,36.6,93.6
|
||||||
2026H1,200.0,8.1,42.6,165.5
|
2026H1,201.2,8.2,42.9,166.5
|
||||||
|
|||||||
|
@@ -0,0 +1,138 @@
|
|||||||
|
# 财报图表项目交接说明(2026-08-28)
|
||||||
|
|
||||||
|
## 1. 当前状态
|
||||||
|
|
||||||
|
项目已迁移到 **Vite + Vanilla JavaScript + ECharts**,本地开发地址为 `http://localhost:8899`。页面支持模块/CSS 热更新;修改 `manifest.json` 或 CSV 后会整页刷新。当前泡泡玛特已配置 **22 张图表**,另有 **5 张示例图表**;完整回归与生产构建均已通过。
|
||||||
|
|
||||||
|
当前页面结构:公司选择 → 分析分类 Tab → 当前分类图表卡片 → 右侧图表锚点导航。泡泡玛特的“成长性”已经并入“盈利能力”,不再单独保留该分类。
|
||||||
|
|
||||||
|
## 2. 启动与验证
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /Users/joey-xd/sites/vest-tools/smart-fin-chart
|
||||||
|
pnpm install # 仅首次或依赖变化后执行
|
||||||
|
pnpm dev # 打开 http://localhost:8899
|
||||||
|
pnpm test # 27 张图表的 ECharts SSR 回归
|
||||||
|
pnpm build # 生产构建
|
||||||
|
```
|
||||||
|
|
||||||
|
项目根目录另有可双击执行的 [`start.command`](../start.command),会启动 Vite 并打开本地地址。
|
||||||
|
|
||||||
|
截至本次交接,`pnpm test` 的结果是 **27 张成功 / 0 张失败**。其中 `ppmt-ip-share` 与 `ppmt-ip-share-history` 会提示 `legend.top=middle`,这是两张右侧纵向图例的预期配置,不是故障。
|
||||||
|
|
||||||
|
## 3. 架构与单一数据源
|
||||||
|
|
||||||
|
### 图表声明
|
||||||
|
|
||||||
|
[`manifest.json`](../manifest.json) 是公司、分类、图表与 CSV 的唯一声明源:
|
||||||
|
|
||||||
|
- `company.chartCatalog`:每个图表只声明一次(图表 key、CSV、画布高度、导出文件名)。
|
||||||
|
- `company.tabs[].chartRefs`:分类只引用图表 ID;概览与详细分类可以引用同一个 ID,不复制图表代码或数据配置。
|
||||||
|
- `js/main.js`:读取 manifest、把 `chartRefs` 解析为视图实例;同一图在概览和详细分类中使用同一 chart builder。
|
||||||
|
- `js/charts/index.js`:chart key 与图表构建函数的注册表。
|
||||||
|
|
||||||
|
这项设计已经用于“概览”:概览展示的营收利润增速、盈利能力、IP 占比、区域渠道分布,在原分类中仍保留,且复用同一组件和同一 CSV。
|
||||||
|
|
||||||
|
### 新增图表的标准步骤
|
||||||
|
|
||||||
|
1. 在 `data/<company>/` 新增或更新运行时 CSV。
|
||||||
|
2. 在 `js/charts/` 新建 chart builder,并从 `js/theme.js` 导入公共主题与系列构造器。
|
||||||
|
3. 在 `js/charts/index.js` 注册 key。
|
||||||
|
4. 在 `manifest.json` 的 `chartCatalog` 中声明一次,再在所需 Tab 的 `chartRefs` 中引用。
|
||||||
|
5. 普通图表运行 `node test/regress.mjs <chart-id>`;若改了 `theme.js`、`csv.js`、manifest 等公共部分,运行 `pnpm test`,然后 `pnpm build`。
|
||||||
|
|
||||||
|
详细开发约定见 [`CLAUDE.md`](../CLAUDE.md),方案背景见 [`方案设计.md`](方案设计.md)。
|
||||||
|
|
||||||
|
## 4. 泡泡玛特当前分类与图表
|
||||||
|
|
||||||
|
| 分类 | 图表 |
|
||||||
|
| --- | --- |
|
||||||
|
| 概览 | 营收到利润的增速对比、盈利能力、主要 IP 收入占比、区域渠道分布 |
|
||||||
|
| 盈利能力 | 营收到利润的增速对比、核心利润及同比增速、核心利润率、净利率同比拆解、利润流向、经营现金流与净利润、三大现金流与现金净增加 |
|
||||||
|
| 费用率 | 利润率与费用率拆解、费用结构与费用率 |
|
||||||
|
| 资产负债 | 资产结构演变、现金/负债/净营运资本、营运资本效率 |
|
||||||
|
| IP 与品类 | 主要 IP 收入趋势、主要 IP 收入占比、各 IP 半年收入占比、除 THE MONSTERS 外 IP 收入及同比、品类收入结构 |
|
||||||
|
| 区域渠道 | 区域渠道分布、区域渠道同比增速、区域渠道同比增速趋势、渠道收入结构与增速 |
|
||||||
|
|
||||||
|
## 5. 数据状态与口径
|
||||||
|
|
||||||
|
### 原始数据
|
||||||
|
|
||||||
|
用户提供的 8 份 2026-08-28 原始 CSV 已原样保存到 [`data/ppmt/raw/`](../data/ppmt/raw/):资产负债表、利润表、现金流量表、财务指标、营收构成、员工、股本变动、分红分析。目录中也保留了早期 `ppmt.xlsx` 与 `2026H1.pdf`。
|
||||||
|
|
||||||
|
运行时图表 CSV 位于 [`data/ppmt/`](../data/ppmt/);原始文件不直接被浏览器读取。归一化脚本为 [`scripts/normalize_ppmt_financials.mjs`](../scripts/normalize_ppmt_financials.mjs),可由原始合并报表和财务指标生成以下衍生数据:
|
||||||
|
|
||||||
|
- `ppmt_cash_conversion.csv`
|
||||||
|
- `ppmt_liquidity_working_capital.csv`
|
||||||
|
- `ppmt_cashflow_composition.csv`
|
||||||
|
- `ppmt_working_capital_days.csv`
|
||||||
|
- `ppmt_expense_structure.csv`
|
||||||
|
- `ppmt_channel_revenue.csv`
|
||||||
|
|
||||||
|
### 必须注意的口径
|
||||||
|
|
||||||
|
- CSV 内的 `202xH2` 在利润表/部分财务表中代表**全年累计**,显示层会写成 `xx全年`;不可当作单独的下半年。
|
||||||
|
- `ppmt_ip_share_history.csv` 的 H2 则是**单半年**,所以该图使用自定义横轴格式,不能套 `periodAxis` 的“全年”转换。
|
||||||
|
- 非 GAAP 净利率是派生指标:非 GAAP 净利 ÷ 营业收入;2020H1/H2 缺失时暂用归母净利替代,脚注已标注。
|
||||||
|
- 净利率同比拆解为税盾换算后的百分点贡献;改动后应验证首尾闭合误差小于 0.05pct。
|
||||||
|
|
||||||
|
## 6. 近期完成的修改
|
||||||
|
|
||||||
|
### 页面与交互
|
||||||
|
|
||||||
|
- 使用 Vite 的 vanilla 模式,取代静态服务器,支持热更新。
|
||||||
|
- 页面通过 `localhost` 访问,避免 `file://` 读取 CSV 的 CORS 问题。
|
||||||
|
- 移除了宽而无信息价值的“财报图表”标题栏;“重新加载数据”改为固定位置的刷新图标按钮。
|
||||||
|
- 左侧菜单断点调整为 `880px`。
|
||||||
|
- 区域渠道同比增速趋势热力图:绘图区底部留白调整为 `155px`;控制器与横轴不重叠;高/低增长颜色的语义已调整为“红色更代表高增长”。
|
||||||
|
|
||||||
|
### 图表组织与数据
|
||||||
|
|
||||||
|
- 概览改为复用重要图表,不搬走原分类里的图表,避免两份图表代码逐渐不一致。
|
||||||
|
- “成长性”内容并入“盈利能力”,分类导航和方案文档已同步。
|
||||||
|
- 基于最新原始数据新增 6 张高优先级图表:经营现金流与净利润、现金/负债/净营运资本、三大现金流与现金净增加、营运资本效率、费用结构与费用率、渠道收入结构与增速。
|
||||||
|
|
||||||
|
### 图例与标签
|
||||||
|
|
||||||
|
- 图例图标统一交由 ECharts 按系列类型渲染:柱状图为矩形,折线图为“短线 + 空心圆”;统一图例图标宽度为 `20px`。
|
||||||
|
- `barSeries()` 默认强制展示柱状标签,避免小柱标签被自动隐藏。
|
||||||
|
- `lineDataWithSafeLabels()` 会根据折线值与柱值的接近程度调整标签距离;本次增加 `closePosition` 参数,可把接近柱顶的折线标签切到柱体另一侧。
|
||||||
|
- “营运资本效率”四条折线已全部显示数值,并按同期间相邻数值的间隔决定朝上/朝下,降低遮挡。
|
||||||
|
- “现金、负债与净营运资本”两组柱和净营运资本折线均显示数值。
|
||||||
|
|
||||||
|
### 配色:已确认方向为「轻快消费」
|
||||||
|
|
||||||
|
主题文件 [`js/theme.js`](../js/theme.js) 中的正式色板:
|
||||||
|
|
||||||
|
- `FINANCIAL_COLORS`:青绿经营现金流、蓝色投资/现金、琥珀融资/负债、珊瑚红净现金增加或关键压力项、暖橙净利润、紫色转化率/同比强调。
|
||||||
|
- `CHANNEL_COLORS`:渠道收入和增速的固定语义色。
|
||||||
|
- `BRIDGE_COLORS`:瀑布图固定为蓝色基准、青绿色正向贡献、珊瑚红负向拖累。
|
||||||
|
|
||||||
|
最近已按该方向调整:
|
||||||
|
|
||||||
|
- 净利率同比拆解:蓝色基准、青绿色正向、珊瑚红负向。
|
||||||
|
- 核心利润及同比增速:暖橙利润柱、紫色同比线。
|
||||||
|
- 经营现金流与净利润、三大现金流、渠道收入结构与增速已采用该色系。
|
||||||
|
- 现金/负债/净营运资本:蓝、琥珀、青绿。
|
||||||
|
- 营运资本效率:琥珀、蓝、紫、珊瑚红。
|
||||||
|
|
||||||
|
后续新增图表必须优先复用主题语义色,不要在单个图表文件里重复手写同语义的十六进制颜色。具体规则见 [`CLAUDE.md`](../CLAUDE.md) 的“配色”部分。
|
||||||
|
|
||||||
|
## 7. 下一步建议(尚未开始)
|
||||||
|
|
||||||
|
按开发优先级建议:
|
||||||
|
|
||||||
|
1. **人工视觉巡检**:重点确认资产负债页新增加的全量标签在桌面和窄屏下是否仍有重叠;当前已通过 SVG 回归,仍应以真实浏览器视觉为准。
|
||||||
|
2. **补齐数据链路**:当前员工、股本变动、分红分析原始数据已归档,但尚未生成对应运行时 CSV 或图表。可优先评估“人均营收/人均利润”“分红与派息率”“股本/期权稀释”等图表。
|
||||||
|
3. **更新对外 README**:README 的图表清单仍停在较早阶段,未完整列出新增的 6 张 PPMT 图,也仍写有“raw 仅 PDF/XLSX”的旧描述;这不影响运行,但应在对外使用前更新。
|
||||||
|
4. **通用财务模板后置**:只有至少两家公司需要同一图型、字段映射和口径都稳定时,再抽取 `template(rows, meta, { width, height }) => option`;当前保持“一图一文件”更稳妥。
|
||||||
|
|
||||||
|
## 8. 给下一位 AI 的工作约束
|
||||||
|
|
||||||
|
- 不要用 npm;使用 pnpm。
|
||||||
|
- 不要把概览图复制出第二套 chart 配置;复用 `manifest.json` 的同一 `chartRef`。
|
||||||
|
- 主题、图例、字体、轴和脚注均从 `js/theme.js` 复用;优先 `standardLegend()`、`lineSeries()`、`barSeries()`。
|
||||||
|
- 不要手工设置 `legend.icon`,否则会破坏柱/折线图例的统一样式。
|
||||||
|
- 修改公共模块(特别是 `theme.js`、`csv.js`、manifest)必须跑 `pnpm test` 与 `pnpm build`。
|
||||||
|
- 保留已有用户修改;不要使用 `git reset --hard` 或覆盖无关文件。
|
||||||
|
|
||||||
+6
-1
@@ -147,7 +147,9 @@ js/main.js + js/ui.js ← 解析引用、渲染三栏、导出和
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
页面仅在声明多个视图时显示切换按钮,并将选择写入 URL query。当前泡泡玛特图表没有强制启用这一功能;基础能力已保留,等真实数据需要时再登记。
|
页面仅在声明多个视图时显示切换按钮,并将选择写入 URL query;尚未启用多视图的图表维持单一 CSV,不显示控制器。
|
||||||
|
|
||||||
|
泡泡玛特已在 15 张连续图启用该能力:盈利能力、营收到利润的增速对比、核心利润及同比增速、核心利润率、利润率与费用率拆解、资产结构演变、主要 IP 收入趋势、各 IP 收入占比、除 THE MONSTERS 外 IP 收入及同比、品类收入结构、经营现金流与净利润、现金/负债/净营运资本、三大现金流与现金净增加、费用结构与费用率、渠道收入结构与增速。默认显示“全部”,并提供“年度 / 半年”;其原始披露没有可靠 Q1/Q3 数据,故不展示“季度”。其中 `FY` 为全年、`H1/H2` 为单独半年,流量类指标的 H2 在数据归一化阶段由 `FY − H1` 生成,比率与同比也在数据层重算;资产负债表类则为期末余额,H2 对应年末时点而非累计金额。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -156,6 +158,9 @@ js/main.js + js/ui.js ← 解析引用、渲染三栏、导出和
|
|||||||
新增图表必须复用 `theme.js` 的 `FONT`、`PALETTE`、`LAYOUT`、`periodAxis()`、`standardLegend()`、`lineSeries()`、`barSeries()`、`footnote()` 与 `watermarks()`,而不是在单个图表中重复手写基础样式。
|
新增图表必须复用 `theme.js` 的 `FONT`、`PALETTE`、`LAYOUT`、`periodAxis()`、`standardLegend()`、`lineSeries()`、`barSeries()`、`footnote()` 与 `watermarks()`,而不是在单个图表中重复手写基础样式。
|
||||||
|
|
||||||
- 财务含义相同的系列应使用固定语义色;IP、区域、品类等多分类数据使用统一分类色板。
|
- 财务含义相同的系列应使用固定语义色;IP、区域、品类等多分类数据使用统一分类色板。
|
||||||
|
- 柱线组合图采用已确认的“轻快消费”色板:柱状系列用不同色相承载分类,折线只保留一条高辨识度强调色。现金流、利润与同比使用 `FINANCIAL_COLORS`,渠道使用 `CHANNEL_COLORS`;瀑布图使用 `BRIDGE_COLORS`(蓝色基准、青绿色正向、珊瑚红负向)。新图不得在图表文件内散落同等语义的十六进制颜色。
|
||||||
|
- 一张图只保留一个视觉主角。辅助柱使用中等饱和度,避免多个红、橙、紫同时争夺注意力;分类图的相邻系列须同时拉开色相与明度。
|
||||||
|
- 颜色不是唯一编码:折线还须依靠线宽、实虚线、空心圆点和标签优先级;柱线混合图的折线标签使用 `lineDataWithSafeLabels()` 等通用避让规则,避免文字压在柱端。
|
||||||
- 新图的业务差异由 chart 文件表达;统一视觉由主题文件表达;展示位置与 CSV 由 manifest 表达。
|
- 新图的业务差异由 chart 文件表达;统一视觉由主题文件表达;展示位置与 CSV 由 manifest 表达。
|
||||||
- 新增或调整图表后,必须检查标签、图例、脚注和控制器是否重叠,以及颜色与文字是否有足够对比度。
|
- 新增或调整图表后,必须检查标签、图例、脚注和控制器是否重叠,以及颜色与文字是否有足够对比度。
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,13 @@ import { cashflowCompositionChart } from './ppmt_cashflow_composition.js';
|
|||||||
import { workingCapitalDaysChart } from './ppmt_working_capital_days.js';
|
import { workingCapitalDaysChart } from './ppmt_working_capital_days.js';
|
||||||
import { expenseStructureChart } from './ppmt_expense_structure.js';
|
import { expenseStructureChart } from './ppmt_expense_structure.js';
|
||||||
import { channelRevenueChart } from './ppmt_channel_revenue.js';
|
import { channelRevenueChart } from './ppmt_channel_revenue.js';
|
||||||
|
import { storesRetailChart } from './ppmt_stores_retail.js';
|
||||||
|
import { storesRobotChart } from './ppmt_stores_robot.js';
|
||||||
|
import { regionShareChart } from './ppmt_region_share.js';
|
||||||
|
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';
|
||||||
|
|
||||||
/** 图表构建函数注册表。manifest 只引用这里的 key,不直接引用 JS 函数。 */
|
/** 图表构建函数注册表。manifest 只引用这里的 key,不直接引用 JS 函数。 */
|
||||||
export const chartRegistry = {
|
export const chartRegistry = {
|
||||||
@@ -55,6 +62,13 @@ export const chartRegistry = {
|
|||||||
'ppmt-working-capital-days': workingCapitalDaysChart,
|
'ppmt-working-capital-days': workingCapitalDaysChart,
|
||||||
'ppmt-expense-structure': expenseStructureChart,
|
'ppmt-expense-structure': expenseStructureChart,
|
||||||
'ppmt-channel-revenue': channelRevenueChart,
|
'ppmt-channel-revenue': channelRevenueChart,
|
||||||
|
'ppmt-stores-retail': storesRetailChart,
|
||||||
|
'ppmt-stores-robot': storesRobotChart,
|
||||||
|
'ppmt-region-share': regionShareChart,
|
||||||
|
'ppmt-inventory': inventoryBalanceChart,
|
||||||
|
'ppmt-stores-retail-share': storesRetailShareChart,
|
||||||
|
'ppmt-retail-store-revenue': retailStoreRevenueChart,
|
||||||
|
'ppmt-robot-store-revenue': robotStoreRevenueChart,
|
||||||
};
|
};
|
||||||
|
|
||||||
export function getChartBuilder(key) {
|
export function getChartBuilder(key) {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { col } from '../csv.js';
|
import { col } from '../csv.js';
|
||||||
import {
|
import {
|
||||||
PALETTE, FONT, axisCommon, periodAxis, titleStyle, legendStyle,
|
PALETTE, FONT, axisCommon, standalonePeriodAxis, titleStyle, legendStyle,
|
||||||
|
financialViewLabel,
|
||||||
lineSeries, footnote, watermarks, LAYOUT,
|
lineSeries, footnote, watermarks, LAYOUT,
|
||||||
} from '../theme.js';
|
} from '../theme.js';
|
||||||
|
|
||||||
@@ -8,15 +9,15 @@ import {
|
|||||||
* 泡泡玛特资产结构演变图
|
* 泡泡玛特资产结构演变图
|
||||||
* 堆叠柱状图展示资产配置,折线图展示总资产增长
|
* 堆叠柱状图展示资产配置,折线图展示总资产增长
|
||||||
*/
|
*/
|
||||||
export function assetsChart(rows, { width, height }) {
|
export function assetsChart(rows, { width, height, view = 'all' }) {
|
||||||
const periods = col(rows, 'period');
|
const periods = col(rows, 'period');
|
||||||
|
|
||||||
return {
|
return {
|
||||||
backgroundColor: '#fff',
|
backgroundColor: '#fff',
|
||||||
title: {
|
title: {
|
||||||
...titleStyle,
|
...titleStyle,
|
||||||
text: '泡泡玛特资产结构演变(2020H1-2026H1)',
|
text: `泡泡玛特资产结构演变(${financialViewLabel(view)})`,
|
||||||
subtext: '单位:亿元',
|
subtext: '单位:亿元;资产负债表为期末余额,H2 为年末时点而非全年累计',
|
||||||
},
|
},
|
||||||
legend: {
|
legend: {
|
||||||
...legendStyle,
|
...legendStyle,
|
||||||
@@ -29,7 +30,7 @@ export function assetsChart(rows, { width, height }) {
|
|||||||
axisPointer: { type: 'shadow' },
|
axisPointer: { type: 'shadow' },
|
||||||
textStyle: { fontFamily: FONT },
|
textStyle: { fontFamily: FONT },
|
||||||
},
|
},
|
||||||
xAxis: { ...periodAxis, data: periods },
|
xAxis: { ...standalonePeriodAxis, data: periods },
|
||||||
yAxis: [
|
yAxis: [
|
||||||
{
|
{
|
||||||
...axisCommon,
|
...axisCommon,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { col } from '../csv.js';
|
import { col } from '../csv.js';
|
||||||
import {
|
import {
|
||||||
PALETTE, FONT, axisCommon, titleStyle, footnote, watermarks,
|
PALETTE, BRIDGE_COLORS, FONT, axisCommon, titleStyle, footnote, watermarks,
|
||||||
} from '../theme.js';
|
} from '../theme.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -13,12 +13,6 @@ import {
|
|||||||
* 税率变化单独成项。恒等式 净利率 = 税前利润率 × (1-税率),
|
* 税率变化单独成项。恒等式 净利率 = 税前利润率 × (1-税率),
|
||||||
* Δ净 = Δ税前×(1-t₁) − 税前₂×Δt,实测闭合误差 0.00pct。
|
* Δ净 = Δ税前×(1-t₁) − 税前₂×Δt,实测闭合误差 0.00pct。
|
||||||
*/
|
*/
|
||||||
const COLOR = {
|
|
||||||
total: '#2F5C85', // 首尾锚点
|
|
||||||
neg: '#C0392B', // 拖累
|
|
||||||
pos: '#1A9E5C', // 利好
|
|
||||||
};
|
|
||||||
|
|
||||||
export function ppmtBridgeChart(rows, { width, height }) {
|
export function ppmtBridgeChart(rows, { width, height }) {
|
||||||
const items = col(rows, 'item');
|
const items = col(rows, 'item');
|
||||||
const types = col(rows, 'type');
|
const types = col(rows, 'type');
|
||||||
@@ -92,7 +86,7 @@ export function ppmtBridgeChart(rows, { width, height }) {
|
|||||||
barWidth: 51,
|
barWidth: 51,
|
||||||
data: values.map((v, i) => ({
|
data: values.map((v, i) => ({
|
||||||
value: v,
|
value: v,
|
||||||
itemStyle: { color: COLOR[types[i]] },
|
itemStyle: { color: BRIDGE_COLORS[types[i]] },
|
||||||
})),
|
})),
|
||||||
label: {
|
label: {
|
||||||
show: true,
|
show: true,
|
||||||
|
|||||||
@@ -1,19 +1,23 @@
|
|||||||
import { col } from '../csv.js';
|
import { col } from '../csv.js';
|
||||||
import {
|
import {
|
||||||
FINANCIAL_COLORS, PALETTE, FONT, axisCommon, periodAxis, titleStyle, legendStyle,
|
FINANCIAL_COLORS, PALETTE, FONT, axisCommon, standalonePeriodAxis, titleStyle, legendStyle,
|
||||||
|
financialViewDisclosure, financialViewLabel,
|
||||||
lineSeries, footnote, watermarks, LAYOUT,
|
lineSeries, footnote, watermarks, LAYOUT,
|
||||||
} from '../theme.js';
|
} from '../theme.js';
|
||||||
|
|
||||||
/** 泡泡玛特经营现金流与净利润:观察利润的现金兑现能力。 */
|
/** 泡泡玛特经营现金流与净利润:观察利润的现金兑现能力。 */
|
||||||
export function cashConversionChart(rows, { width, height }) {
|
export function cashConversionChart(rows, { width, height, view = 'all' }) {
|
||||||
const periods = col(rows, 'period');
|
const periods = col(rows, 'period');
|
||||||
|
const conversionMax = Math.max(...col(rows, 'cash_conversion').filter(Number.isFinite));
|
||||||
|
const conversionAxisMax = Math.max(200, Math.ceil((conversionMax * 1.1) / 100) * 100);
|
||||||
|
const barWidth = view === 'all' ? 14 : view === 'halfyear' ? 24.96 : 31.2;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
backgroundColor: '#fff',
|
backgroundColor: '#fff',
|
||||||
title: {
|
title: {
|
||||||
...titleStyle,
|
...titleStyle,
|
||||||
text: '泡泡玛特经营现金流与净利润(年度)',
|
text: `泡泡玛特经营现金流与净利润(${financialViewLabel(view)})`,
|
||||||
subtext: '现金转换率 = 经营活动现金流净额 / 净利润;单位:亿元',
|
subtext: ['现金转换率 = 经营活动现金流净额 / 净利润;单位:亿元', financialViewDisclosure(view)].filter(Boolean).join('\n'),
|
||||||
},
|
},
|
||||||
legend: {
|
legend: {
|
||||||
...legendStyle,
|
...legendStyle,
|
||||||
@@ -27,7 +31,7 @@ export function cashConversionChart(rows, { width, height }) {
|
|||||||
textStyle: { fontFamily: FONT },
|
textStyle: { fontFamily: FONT },
|
||||||
valueFormatter: (value) => `${value.toFixed(1)}`,
|
valueFormatter: (value) => `${value.toFixed(1)}`,
|
||||||
},
|
},
|
||||||
xAxis: { ...periodAxis, data: periods },
|
xAxis: { ...standalonePeriodAxis, data: periods },
|
||||||
yAxis: [
|
yAxis: [
|
||||||
{
|
{
|
||||||
...axisCommon,
|
...axisCommon,
|
||||||
@@ -48,8 +52,8 @@ export function cashConversionChart(rows, { width, height }) {
|
|||||||
nameRotate: -90,
|
nameRotate: -90,
|
||||||
nameTextStyle: { color: PALETTE.grey, fontSize: 13, fontFamily: FONT },
|
nameTextStyle: { color: PALETTE.grey, fontSize: 13, fontFamily: FONT },
|
||||||
min: 0,
|
min: 0,
|
||||||
max: 200,
|
max: conversionAxisMax,
|
||||||
interval: 50,
|
interval: conversionAxisMax / 4,
|
||||||
axisLabel: { ...axisCommon.axisLabel, formatter: '{value}%' },
|
axisLabel: { ...axisCommon.axisLabel, formatter: '{value}%' },
|
||||||
splitLine: { show: false },
|
splitLine: { show: false },
|
||||||
},
|
},
|
||||||
@@ -59,7 +63,7 @@ export function cashConversionChart(rows, { width, height }) {
|
|||||||
name: '经营活动现金流净额',
|
name: '经营活动现金流净额',
|
||||||
type: 'bar',
|
type: 'bar',
|
||||||
data: col(rows, 'operating_cash_flow'),
|
data: col(rows, 'operating_cash_flow'),
|
||||||
barWidth: 31.2,
|
barWidth,
|
||||||
itemStyle: { color: FINANCIAL_COLORS.operatingCashFlow },
|
itemStyle: { color: FINANCIAL_COLORS.operatingCashFlow },
|
||||||
label: { show: true, position: 'top', color: PALETTE.ink, fontFamily: FONT, fontSize: 11, formatter: (p) => p.value.toFixed(1) },
|
label: { show: true, position: 'top', color: PALETTE.ink, fontFamily: FONT, fontSize: 11, formatter: (p) => p.value.toFixed(1) },
|
||||||
},
|
},
|
||||||
@@ -67,7 +71,7 @@ export function cashConversionChart(rows, { width, height }) {
|
|||||||
name: '净利润',
|
name: '净利润',
|
||||||
type: 'bar',
|
type: 'bar',
|
||||||
data: col(rows, 'net_profit'),
|
data: col(rows, 'net_profit'),
|
||||||
barWidth: 31.2,
|
barWidth,
|
||||||
itemStyle: { color: FINANCIAL_COLORS.netProfit },
|
itemStyle: { color: FINANCIAL_COLORS.netProfit },
|
||||||
label: { show: true, position: 'top', color: PALETTE.ink, fontFamily: FONT, fontSize: 11, formatter: (p) => p.value.toFixed(1) },
|
label: { show: true, position: 'top', color: PALETTE.ink, fontFamily: FONT, fontSize: 11, formatter: (p) => p.value.toFixed(1) },
|
||||||
},
|
},
|
||||||
@@ -85,7 +89,7 @@ export function cashConversionChart(rows, { width, height }) {
|
|||||||
],
|
],
|
||||||
graphic: [
|
graphic: [
|
||||||
...watermarks(width, height),
|
...watermarks(width, height),
|
||||||
footnote('数据来源:泡泡玛特合并现金流量表、利润表(年度口径)| @思考的Joey'),
|
footnote('数据来源:泡泡玛特合并现金流量表、利润表;H2为单独半年 | @思考的Joey'),
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
import { col } from '../csv.js';
|
import { col } from '../csv.js';
|
||||||
import {
|
import {
|
||||||
FINANCIAL_COLORS, PALETTE, FONT, axisCommon, periodAxis, titleStyle, standardLegend,
|
FINANCIAL_COLORS, PALETTE, FONT, axisCommon, standalonePeriodAxis, titleStyle, standardLegend,
|
||||||
|
financialViewDisclosure, financialViewLabel,
|
||||||
lineDataWithSafeLabels,
|
lineDataWithSafeLabels,
|
||||||
lineSeries, footnote, watermarks, LAYOUT,
|
lineSeries, footnote, watermarks, LAYOUT,
|
||||||
} from '../theme.js';
|
} from '../theme.js';
|
||||||
|
|
||||||
/** 泡泡玛特三大现金流:展示经营造血、投资投入与筹资变化。 */
|
/** 泡泡玛特三大现金流:展示经营造血、投资投入与筹资变化。 */
|
||||||
export function cashflowCompositionChart(rows, { width, height }) {
|
export function cashflowCompositionChart(rows, { width, height, view = 'all' }) {
|
||||||
|
const barWidth = view === 'all' ? 10 : view === 'halfyear' ? 18.2 : 26;
|
||||||
const netCashIncrease = lineDataWithSafeLabels(rows, {
|
const netCashIncrease = lineDataWithSafeLabels(rows, {
|
||||||
lineKey: 'net_cash_increase',
|
lineKey: 'net_cash_increase',
|
||||||
barKeys: ['operating_cash_flow', 'investing_cash_flow', 'financing_cash_flow'],
|
barKeys: ['operating_cash_flow', 'investing_cash_flow', 'financing_cash_flow'],
|
||||||
@@ -16,8 +18,8 @@ export function cashflowCompositionChart(rows, { width, height }) {
|
|||||||
backgroundColor: '#fff',
|
backgroundColor: '#fff',
|
||||||
title: {
|
title: {
|
||||||
...titleStyle,
|
...titleStyle,
|
||||||
text: '泡泡玛特三大现金流与现金净增加(年度)',
|
text: `泡泡玛特三大现金流与现金净增加(${financialViewLabel(view)})`,
|
||||||
subtext: '单位:亿元;正值为流入,负值为流出',
|
subtext: ['单位:亿元;正值为流入,负值为流出', financialViewDisclosure(view)].filter(Boolean).join('\n'),
|
||||||
},
|
},
|
||||||
// 不手工指定 icon,让 ECharts 按系列类型生成:柱为矩形、折线为短线 + 空心圆。
|
// 不手工指定 icon,让 ECharts 按系列类型生成:柱为矩形、折线为短线 + 空心圆。
|
||||||
legend: standardLegend(['经营活动现金流', '投资活动现金流', '筹资活动现金流', '现金净增加']),
|
legend: standardLegend(['经营活动现金流', '投资活动现金流', '筹资活动现金流', '现金净增加']),
|
||||||
@@ -28,7 +30,7 @@ export function cashflowCompositionChart(rows, { width, height }) {
|
|||||||
textStyle: { fontFamily: FONT },
|
textStyle: { fontFamily: FONT },
|
||||||
valueFormatter: (value) => `${value.toFixed(1)} 亿元`,
|
valueFormatter: (value) => `${value.toFixed(1)} 亿元`,
|
||||||
},
|
},
|
||||||
xAxis: { ...periodAxis, data: col(rows, 'period') },
|
xAxis: { ...standalonePeriodAxis, data: col(rows, 'period') },
|
||||||
yAxis: {
|
yAxis: {
|
||||||
...axisCommon,
|
...axisCommon,
|
||||||
type: 'value',
|
type: 'value',
|
||||||
@@ -38,9 +40,9 @@ export function cashflowCompositionChart(rows, { width, height }) {
|
|||||||
nameTextStyle: { color: PALETTE.grey, fontSize: 13, fontFamily: FONT },
|
nameTextStyle: { color: PALETTE.grey, fontSize: 13, fontFamily: FONT },
|
||||||
},
|
},
|
||||||
series: [
|
series: [
|
||||||
{ name: '经营活动现金流', type: 'bar', data: col(rows, 'operating_cash_flow'), barWidth: 26, itemStyle: { color: FINANCIAL_COLORS.operatingCashFlow } },
|
{ name: '经营活动现金流', type: 'bar', data: col(rows, 'operating_cash_flow'), barWidth, itemStyle: { color: FINANCIAL_COLORS.operatingCashFlow } },
|
||||||
{ name: '投资活动现金流', type: 'bar', data: col(rows, 'investing_cash_flow'), barWidth: 26, itemStyle: { color: FINANCIAL_COLORS.investingCashFlow } },
|
{ name: '投资活动现金流', type: 'bar', data: col(rows, 'investing_cash_flow'), barWidth, itemStyle: { color: FINANCIAL_COLORS.investingCashFlow } },
|
||||||
{ name: '筹资活动现金流', type: 'bar', data: col(rows, 'financing_cash_flow'), barWidth: 26, itemStyle: { color: FINANCIAL_COLORS.financingCashFlow } },
|
{ name: '筹资活动现金流', type: 'bar', data: col(rows, 'financing_cash_flow'), barWidth, itemStyle: { color: FINANCIAL_COLORS.financingCashFlow } },
|
||||||
{
|
{
|
||||||
...lineSeries({
|
...lineSeries({
|
||||||
name: '现金净增加',
|
name: '现金净增加',
|
||||||
@@ -55,7 +57,7 @@ export function cashflowCompositionChart(rows, { width, height }) {
|
|||||||
],
|
],
|
||||||
graphic: [
|
graphic: [
|
||||||
...watermarks(width, height),
|
...watermarks(width, height),
|
||||||
footnote('数据来源:泡泡玛特合并现金流量表(年度口径)| @思考的Joey'),
|
footnote('数据来源:泡泡玛特合并现金流量表;H2为单独半年 | @思考的Joey'),
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { col } from '../csv.js';
|
import { col } from '../csv.js';
|
||||||
import {
|
import {
|
||||||
PALETTE, FONT, axisCommon, categoryAxis, titleStyle, legendStyle,
|
PALETTE, FONT, axisCommon, standalonePeriodAxis, titleStyle, legendStyle,
|
||||||
|
financialViewDisclosure, financialViewLabel,
|
||||||
footnote, watermarks, LAYOUT,
|
footnote, watermarks, LAYOUT,
|
||||||
} from '../theme.js';
|
} from '../theme.js';
|
||||||
|
|
||||||
@@ -8,7 +9,7 @@ import {
|
|||||||
* 泡泡玛特品类收入结构图
|
* 泡泡玛特品类收入结构图
|
||||||
* 堆叠柱状图展示各品类的收入占比和增长
|
* 堆叠柱状图展示各品类的收入占比和增长
|
||||||
*/
|
*/
|
||||||
export function categoryChart(rows, { width, height }) {
|
export function categoryChart(rows, { width, height, view = 'all' }) {
|
||||||
// 过滤掉表头行("品类"那一行)
|
// 过滤掉表头行("品类"那一行)
|
||||||
const dataRows = rows.filter(r => r.period && r.period !== '品类');
|
const dataRows = rows.filter(r => r.period && r.period !== '品类');
|
||||||
const periods = col(dataRows, 'period');
|
const periods = col(dataRows, 'period');
|
||||||
@@ -17,8 +18,8 @@ export function categoryChart(rows, { width, height }) {
|
|||||||
backgroundColor: '#fff',
|
backgroundColor: '#fff',
|
||||||
title: {
|
title: {
|
||||||
...titleStyle,
|
...titleStyle,
|
||||||
text: '泡泡玛特品类收入结构(2024H1-2025H2)',
|
text: `泡泡玛特品类收入结构(${financialViewLabel(view)})`,
|
||||||
subtext: '单位:亿元',
|
subtext: ['单位:亿元;H2为下半年单期', financialViewDisclosure(view)].filter(Boolean).join('\n'),
|
||||||
},
|
},
|
||||||
legend: {
|
legend: {
|
||||||
...legendStyle,
|
...legendStyle,
|
||||||
@@ -41,14 +42,7 @@ export function categoryChart(rows, { width, height }) {
|
|||||||
return result;
|
return result;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
xAxis: {
|
xAxis: { ...standalonePeriodAxis, data: periods },
|
||||||
...categoryAxis,
|
|
||||||
data: periods,
|
|
||||||
axisLabel: {
|
|
||||||
...categoryAxis.axisLabel,
|
|
||||||
formatter: (value) => String(value).replace(/^20/, ''),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
yAxis: {
|
yAxis: {
|
||||||
...axisCommon,
|
...axisCommon,
|
||||||
type: 'value',
|
type: 'value',
|
||||||
|
|||||||
@@ -1,24 +1,86 @@
|
|||||||
import { col } from '../csv.js';
|
import { col } from '../csv.js';
|
||||||
import {
|
import {
|
||||||
CHANNEL_COLORS, PALETTE, FONT, axisCommon, periodAxis, titleStyle, legendStyle,
|
CHANNEL_COLORS, PALETTE, FONT, axisCommon, standalonePeriodAxis, titleStyle, legendStyle,
|
||||||
|
financialViewDisclosure, financialViewLabel,
|
||||||
lineSeries, footnote, watermarks, LAYOUT,
|
lineSeries, footnote, watermarks, LAYOUT,
|
||||||
} from '../theme.js';
|
} from '../theme.js';
|
||||||
|
|
||||||
/** 泡泡玛特渠道收入结构与增速。 */
|
/** 泡泡玛特渠道收入结构与增速。 */
|
||||||
export function channelRevenueChart(rows, { width, height }) {
|
const channels = [
|
||||||
const channels = [
|
|
||||||
{ key: 'retail', name: '零售店', color: CHANNEL_COLORS.retail },
|
{ key: 'retail', name: '零售店', color: CHANNEL_COLORS.retail },
|
||||||
{ key: 'online', name: '线上', color: CHANNEL_COLORS.online },
|
{ key: 'online', name: '线上', color: CHANNEL_COLORS.online },
|
||||||
{ key: 'robot_store', name: '机器人商店', color: CHANNEL_COLORS.robotStore },
|
{ key: 'robot_store', name: '机器人商店', color: CHANNEL_COLORS.robotStore },
|
||||||
{ key: 'wholesale', name: '批发', color: CHANNEL_COLORS.wholesale },
|
{ key: 'wholesale', name: '批发', color: CHANNEL_COLORS.wholesale },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// 段内标签文字色:浅色底(批发金)用深字,其余深色底用白字
|
||||||
|
const INSIDE_LABEL_COLORS = {
|
||||||
|
retail: '#fff',
|
||||||
|
online: '#fff',
|
||||||
|
robot_store: '#fff',
|
||||||
|
wholesale: PALETTE.ink,
|
||||||
|
};
|
||||||
|
|
||||||
|
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 yoyValues = col(rows, 'revenue_yoy').filter(Number.isFinite);
|
||||||
|
// 全部/半年视图存在 2022H2 的负增长,右轴需包含负区间,避免折线被 0 轴裁切。
|
||||||
|
const yoyMin = Math.min(0, Math.floor(Math.min(...yoyValues) / 50) * 50);
|
||||||
|
const yoyMax = Math.max(50, Math.ceil(Math.max(...yoyValues) / 50) * 50);
|
||||||
|
// 绘图区像素高:画布高 − grid top 145 − grid bottom 90
|
||||||
|
const pxPerUnit = (height - LAYOUT.gridTop - 90) / axisMax;
|
||||||
|
|
||||||
|
// 每根柱里放不下的矮段:标签外置到柱体右侧。同一柱内多个矮段标签
|
||||||
|
// 自下而上保持 12px 间距;整根柱都是矮段时,纵向扇形以柱身中部为中心,
|
||||||
|
// 避免标签整体飘离柱顶。返回 { 渠道序号: offset dy }(dy 为像素,正方向向下)。
|
||||||
|
const fanOutsideLabels = (row) => {
|
||||||
|
const values = channels.map((item) => num(row[item.key]));
|
||||||
|
const centers = [];
|
||||||
|
let cum = 0;
|
||||||
|
values.forEach((value) => {
|
||||||
|
centers.push((cum + value / 2) * pxPerUnit);
|
||||||
|
cum += value;
|
||||||
|
});
|
||||||
|
const barTopPx = cum * pxPerUnit;
|
||||||
|
const tinyIdx = channels
|
||||||
|
.map((_, i) => i)
|
||||||
|
.filter((i) => values[i] !== null && values[i] * pxPerUnit < MIN_INSIDE_PX);
|
||||||
|
if (tinyIdx.length === 0) return {};
|
||||||
|
const ys = tinyIdx.map((i) => centers[i]);
|
||||||
|
for (let k = 1; k < ys.length; k += 1) {
|
||||||
|
ys[k] = Math.max(ys[k], ys[k - 1] + 12);
|
||||||
|
}
|
||||||
|
if (tinyIdx.length === channels.length) {
|
||||||
|
const shift = barTopPx / 2 - (ys[0] + ys[ys.length - 1]) / 2;
|
||||||
|
for (let k = 0; k < ys.length; k += 1) ys[k] += shift;
|
||||||
|
if (ys[0] < 2) {
|
||||||
|
const lift = 2 - ys[0];
|
||||||
|
for (let k = 0; k < ys.length; k += 1) ys[k] += lift;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const offsets = {};
|
||||||
|
tinyIdx.forEach((i, k) => {
|
||||||
|
offsets[i] = centers[i] - ys[k];
|
||||||
|
});
|
||||||
|
return offsets;
|
||||||
|
};
|
||||||
|
|
||||||
|
const rowOffsets = rows.map(fanOutsideLabels);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
backgroundColor: '#fff',
|
backgroundColor: '#fff',
|
||||||
title: {
|
title: {
|
||||||
...titleStyle,
|
...titleStyle,
|
||||||
text: '泡泡玛特渠道收入结构与增速',
|
text: `泡泡玛特渠道收入结构与增速(${financialViewLabel(view)})`,
|
||||||
subtext: '渠道收入为堆叠柱;折线为营业收入同比增速',
|
subtext: ['渠道收入为堆叠柱,柱内及柱侧标注各渠道数值;折线为营业收入同比增速', financialViewDisclosure(view)].filter(Boolean).join('\n'),
|
||||||
},
|
},
|
||||||
legend: {
|
legend: {
|
||||||
...legendStyle,
|
...legendStyle,
|
||||||
@@ -32,7 +94,7 @@ export function channelRevenueChart(rows, { width, height }) {
|
|||||||
textStyle: { fontFamily: FONT },
|
textStyle: { fontFamily: FONT },
|
||||||
valueFormatter: (value) => value.toFixed(1),
|
valueFormatter: (value) => value.toFixed(1),
|
||||||
},
|
},
|
||||||
xAxis: { ...periodAxis, data: col(rows, 'period') },
|
xAxis: { ...standalonePeriodAxis, data: col(rows, 'period') },
|
||||||
yAxis: [
|
yAxis: [
|
||||||
{
|
{
|
||||||
...axisCommon,
|
...axisCommon,
|
||||||
@@ -42,6 +104,8 @@ export function channelRevenueChart(rows, { width, height }) {
|
|||||||
nameGap: 56,
|
nameGap: 56,
|
||||||
nameTextStyle: { color: PALETTE.grey, fontSize: 13, fontFamily: FONT },
|
nameTextStyle: { color: PALETTE.grey, fontSize: 13, fontFamily: FONT },
|
||||||
min: 0,
|
min: 0,
|
||||||
|
max: axisMax,
|
||||||
|
interval: axisMax % 4 === 0 ? axisMax / 4 : axisMax / 5,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
...axisCommon,
|
...axisCommon,
|
||||||
@@ -51,20 +115,45 @@ export function channelRevenueChart(rows, { width, height }) {
|
|||||||
nameGap: 60,
|
nameGap: 60,
|
||||||
nameRotate: -90,
|
nameRotate: -90,
|
||||||
nameTextStyle: { color: PALETTE.grey, fontSize: 13, fontFamily: FONT },
|
nameTextStyle: { color: PALETTE.grey, fontSize: 13, fontFamily: FONT },
|
||||||
min: 0,
|
min: yoyMin,
|
||||||
max: 250,
|
max: yoyMax,
|
||||||
interval: 50,
|
interval: 50,
|
||||||
axisLabel: { ...axisCommon.axisLabel, formatter: '{value}%' },
|
axisLabel: { ...axisCommon.axisLabel, formatter: '{value}%' },
|
||||||
splitLine: { show: false },
|
splitLine: { show: false },
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
series: [
|
series: [
|
||||||
...channels.map((item) => ({
|
...channels.map((item, ci) => ({
|
||||||
name: item.name,
|
name: item.name,
|
||||||
type: 'bar',
|
type: 'bar',
|
||||||
stack: 'channel-revenue',
|
stack: 'channel-revenue',
|
||||||
data: col(rows, item.key),
|
data: rows.map((row, ri) => {
|
||||||
|
const value = num(row[item.key]);
|
||||||
|
if (value === null) return null;
|
||||||
|
const inside = value * pxPerUnit >= MIN_INSIDE_PX;
|
||||||
|
return {
|
||||||
|
value,
|
||||||
|
label: inside
|
||||||
|
? { position: 'inside', color: INSIDE_LABEL_COLORS[item.key] }
|
||||||
|
: {
|
||||||
|
position: 'right',
|
||||||
|
color: PALETTE.ink,
|
||||||
|
distance: 3,
|
||||||
|
offset: [0, rowOffsets[ri][ci] || 0],
|
||||||
|
textBorderColor: '#fff',
|
||||||
|
textBorderWidth: 2,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}),
|
||||||
itemStyle: { color: item.color },
|
itemStyle: { color: item.color },
|
||||||
|
label: {
|
||||||
|
show: true,
|
||||||
|
fontSize: 10,
|
||||||
|
fontWeight: 500,
|
||||||
|
fontFamily: FONT,
|
||||||
|
formatter: (p) => Number(p.value).toFixed(1),
|
||||||
|
overflow: 'none',
|
||||||
|
},
|
||||||
emphasis: { focus: 'series' },
|
emphasis: { focus: 'series' },
|
||||||
})),
|
})),
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { col } from '../csv.js';
|
import { col } from '../csv.js';
|
||||||
import {
|
import {
|
||||||
PALETTE, FONT, axisCommon, periodAxis, titleStyle,
|
PALETTE, FONT, axisCommon, standalonePeriodAxis, titleStyle,
|
||||||
|
financialViewDisclosure, financialViewLabel,
|
||||||
lineSeries, standardLegend, footnote, watermarks, LAYOUT,
|
lineSeries, standardLegend, footnote, watermarks, LAYOUT,
|
||||||
} from '../theme.js';
|
} from '../theme.js';
|
||||||
|
|
||||||
@@ -15,15 +16,15 @@ import {
|
|||||||
*/
|
*/
|
||||||
const LINE_COLOR = '#7B2D42';
|
const LINE_COLOR = '#7B2D42';
|
||||||
|
|
||||||
export function ppmtCoreMarginChart(rows, { width, height }) {
|
export function ppmtCoreMarginChart(rows, { width, height, view = 'all' }) {
|
||||||
const pct = (p) => `${p.value.toFixed(1)}%`;
|
const pct = (p) => `${p.value.toFixed(1)}%`;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
backgroundColor: '#fff',
|
backgroundColor: '#fff',
|
||||||
title: {
|
title: {
|
||||||
...titleStyle,
|
...titleStyle,
|
||||||
text: '泡泡玛特核心利润率(20H1-26H1)',
|
text: `泡泡玛特核心利润率(${financialViewLabel(view)})`,
|
||||||
subtext: '核心利润率 = 毛利率 − 销售费用率 − 管理费用率,剔除汇兑与投资等非主业扰动',
|
subtext: ['核心利润率 = 毛利率 − 销售费用率 − 管理费用率,剔除汇兑与投资等非主业扰动', financialViewDisclosure(view)].filter(Boolean).join('\n'),
|
||||||
},
|
},
|
||||||
legend: standardLegend(['核心利润率'], { itemGap: 32 }),
|
legend: standardLegend(['核心利润率'], { itemGap: 32 }),
|
||||||
grid: { left: 78, right: 60, top: LAYOUT.gridTop, bottom: 90 },
|
grid: { left: 78, right: 60, top: LAYOUT.gridTop, bottom: 90 },
|
||||||
@@ -32,7 +33,7 @@ export function ppmtCoreMarginChart(rows, { width, height }) {
|
|||||||
valueFormatter: (v) => `${v.toFixed(1)}%`,
|
valueFormatter: (v) => `${v.toFixed(1)}%`,
|
||||||
textStyle: { fontFamily: FONT },
|
textStyle: { fontFamily: FONT },
|
||||||
},
|
},
|
||||||
xAxis: { ...periodAxis, data: col(rows, 'period') },
|
xAxis: { ...standalonePeriodAxis, data: col(rows, 'period') },
|
||||||
yAxis: {
|
yAxis: {
|
||||||
...axisCommon,
|
...axisCommon,
|
||||||
type: 'value',
|
type: 'value',
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { col } from '../csv.js';
|
import { col } from '../csv.js';
|
||||||
import {
|
import {
|
||||||
PALETTE, FONT, axisCommon, periodAxis, titleStyle, legendStyle,
|
PALETTE, FINANCIAL_COLORS, FONT, axisCommon, standalonePeriodAxis, titleStyle, legendStyle,
|
||||||
|
financialViewDisclosure, financialViewLabel,
|
||||||
footnote, watermarks, LAYOUT,
|
footnote, watermarks, LAYOUT,
|
||||||
} from '../theme.js';
|
} from '../theme.js';
|
||||||
|
|
||||||
@@ -9,7 +10,7 @@ import {
|
|||||||
* 核心利润 = 毛利 - 销售费用 - 管理费用,反映主业盈利能力去除营销和管理成本后的净额。
|
* 核心利润 = 毛利 - 销售费用 - 管理费用,反映主业盈利能力去除营销和管理成本后的净额。
|
||||||
* 柱状图展示绝对值(亿元),折线图展示同比增速(%)。
|
* 柱状图展示绝对值(亿元),折线图展示同比增速(%)。
|
||||||
*/
|
*/
|
||||||
export function ppmtCoreProfitChart(rows, { width, height }) {
|
export function ppmtCoreProfitChart(rows, { width, height, view = 'all' }) {
|
||||||
const periods = col(rows, 'period');
|
const periods = col(rows, 'period');
|
||||||
const core = col(rows, 'core_profit');
|
const core = col(rows, 'core_profit');
|
||||||
const yoy = col(rows, 'yoy');
|
const yoy = col(rows, 'yoy');
|
||||||
@@ -18,8 +19,8 @@ export function ppmtCoreProfitChart(rows, { width, height }) {
|
|||||||
backgroundColor: '#fff',
|
backgroundColor: '#fff',
|
||||||
title: {
|
title: {
|
||||||
...titleStyle,
|
...titleStyle,
|
||||||
text: '泡泡玛特核心利润及同比增速(20H1-26H1)',
|
text: `泡泡玛特核心利润及同比增速(${financialViewLabel(view)})`,
|
||||||
subtext: '核心利润 = 毛利 - 销售费用 - 管理费用,单位:亿元',
|
subtext: ['核心利润 = 毛利 - 销售费用 - 管理费用,单位:亿元', financialViewDisclosure(view)].filter(Boolean).join('\n'),
|
||||||
},
|
},
|
||||||
legend: {
|
legend: {
|
||||||
...legendStyle,
|
...legendStyle,
|
||||||
@@ -49,7 +50,7 @@ export function ppmtCoreProfitChart(rows, { width, height }) {
|
|||||||
return result;
|
return result;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
xAxis: { ...periodAxis, data: periods },
|
xAxis: { ...standalonePeriodAxis, data: periods },
|
||||||
yAxis: [
|
yAxis: [
|
||||||
{
|
{
|
||||||
// 左轴:核心利润(亿元)
|
// 左轴:核心利润(亿元)
|
||||||
@@ -90,7 +91,7 @@ export function ppmtCoreProfitChart(rows, { width, height }) {
|
|||||||
yAxisIndex: 0,
|
yAxisIndex: 0,
|
||||||
data: core,
|
data: core,
|
||||||
barWidth: 32,
|
barWidth: 32,
|
||||||
itemStyle: { color: PALETTE.navy },
|
itemStyle: { color: FINANCIAL_COLORS.netProfit },
|
||||||
label: {
|
label: {
|
||||||
show: true,
|
show: true,
|
||||||
position: 'top',
|
position: 'top',
|
||||||
@@ -107,12 +108,12 @@ export function ppmtCoreProfitChart(rows, { width, height }) {
|
|||||||
type: 'line',
|
type: 'line',
|
||||||
yAxisIndex: 1,
|
yAxisIndex: 1,
|
||||||
data: yoy,
|
data: yoy,
|
||||||
lineStyle: { color: PALETTE.coral, width: 3.08 }, // 与拆解图毛利率线一致
|
lineStyle: { color: FINANCIAL_COLORS.cashConversion, width: 3.08 },
|
||||||
// 空心圆点:白心 + 彩色描边。图例会自动继承这个样式,
|
// 空心圆点:白心 + 彩色描边。图例会自动继承这个样式,
|
||||||
// 渲染成「短线 + 空心圆」
|
// 渲染成「短线 + 空心圆」
|
||||||
itemStyle: {
|
itemStyle: {
|
||||||
color: '#fff',
|
color: '#fff',
|
||||||
borderColor: PALETTE.coral,
|
borderColor: FINANCIAL_COLORS.cashConversion,
|
||||||
borderWidth: 2.5, // 与 theme.js lineSeries 一致
|
borderWidth: 2.5, // 与 theme.js lineSeries 一致
|
||||||
},
|
},
|
||||||
symbol: 'circle',
|
symbol: 'circle',
|
||||||
@@ -123,7 +124,7 @@ export function ppmtCoreProfitChart(rows, { width, height }) {
|
|||||||
position: 'top',
|
position: 'top',
|
||||||
fontFamily: FONT,
|
fontFamily: FONT,
|
||||||
fontSize: 11,
|
fontSize: 11,
|
||||||
color: PALETTE.coral,
|
color: FINANCIAL_COLORS.cashConversion,
|
||||||
fontWeight: '600',
|
fontWeight: '600',
|
||||||
formatter: (p) => {
|
formatter: (p) => {
|
||||||
if (p.value === null || p.value === '') return '';
|
if (p.value === null || p.value === '') return '';
|
||||||
|
|||||||
@@ -1,17 +1,18 @@
|
|||||||
import { col } from '../csv.js';
|
import { col } from '../csv.js';
|
||||||
import {
|
import {
|
||||||
PALETTE, FONT, axisCommon, periodAxis, titleStyle, legendStyle,
|
PALETTE, FONT, axisCommon, standalonePeriodAxis, titleStyle, legendStyle,
|
||||||
|
financialViewDisclosure, financialViewLabel,
|
||||||
lineSeries, footnote, watermarks, LAYOUT,
|
lineSeries, footnote, watermarks, LAYOUT,
|
||||||
} from '../theme.js';
|
} from '../theme.js';
|
||||||
|
|
||||||
/** 泡泡玛特期间费用结构:观察销售投入、管理成本与费用率的规模效应。 */
|
/** 泡泡玛特期间费用结构:观察销售投入、管理成本与费用率的规模效应。 */
|
||||||
export function expenseStructureChart(rows, { width, height }) {
|
export function expenseStructureChart(rows, { width, height, view = 'all' }) {
|
||||||
return {
|
return {
|
||||||
backgroundColor: '#fff',
|
backgroundColor: '#fff',
|
||||||
title: {
|
title: {
|
||||||
...titleStyle,
|
...titleStyle,
|
||||||
text: '泡泡玛特费用结构与费用率(年度)',
|
text: `泡泡玛特费用结构与费用率(${financialViewLabel(view)})`,
|
||||||
subtext: '销售、管理、财务费用;折线为四项费用率',
|
subtext: ['销售、管理、财务费用;折线为四项费用率', financialViewDisclosure(view)].filter(Boolean).join('\n'),
|
||||||
},
|
},
|
||||||
legend: {
|
legend: {
|
||||||
...legendStyle,
|
...legendStyle,
|
||||||
@@ -25,7 +26,7 @@ export function expenseStructureChart(rows, { width, height }) {
|
|||||||
textStyle: { fontFamily: FONT },
|
textStyle: { fontFamily: FONT },
|
||||||
valueFormatter: (value) => value.toFixed(1),
|
valueFormatter: (value) => value.toFixed(1),
|
||||||
},
|
},
|
||||||
xAxis: { ...periodAxis, data: col(rows, 'period') },
|
xAxis: { ...standalonePeriodAxis, data: col(rows, 'period') },
|
||||||
yAxis: [
|
yAxis: [
|
||||||
{
|
{
|
||||||
...axisCommon,
|
...axisCommon,
|
||||||
@@ -68,7 +69,7 @@ export function expenseStructureChart(rows, { width, height }) {
|
|||||||
],
|
],
|
||||||
graphic: [
|
graphic: [
|
||||||
...watermarks(width, height),
|
...watermarks(width, height),
|
||||||
footnote('数据来源:泡泡玛特合并利润表(年度口径)| @思考的Joey'),
|
footnote('数据来源:泡泡玛特合并利润表;H2为单独半年 | @思考的Joey'),
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { col } from '../csv.js';
|
import { col } from '../csv.js';
|
||||||
import {
|
import {
|
||||||
PALETTE, FONT, axisCommon, periodAxis, titleStyle, standardLegend,
|
PALETTE, FONT, axisCommon, standalonePeriodAxis, titleStyle, standardLegend,
|
||||||
|
financialViewDisclosure, financialViewLabel,
|
||||||
lineSeries, footnote, watermarks, LAYOUT,
|
lineSeries, footnote, watermarks, LAYOUT,
|
||||||
} from '../theme.js';
|
} from '../theme.js';
|
||||||
|
|
||||||
@@ -43,15 +44,15 @@ function labeledData(rows, key) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ppmtGrowthChart(rows, { width, height }) {
|
export function ppmtGrowthChart(rows, { width, height, view = 'all' }) {
|
||||||
const pct = (p) => `${p.value}%`;
|
const pct = (p) => `${p.value}%`;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
backgroundColor: '#fff',
|
backgroundColor: '#fff',
|
||||||
title: {
|
title: {
|
||||||
...titleStyle,
|
...titleStyle,
|
||||||
text: '泡泡玛特营收到利润的增速对比(2020H1-2026H1)',
|
text: `泡泡玛特营收到利润的增速对比(${financialViewLabel(view)})`,
|
||||||
subtext: '同比增速(%),净利增速显著高于营收增速体现利润弹性',
|
subtext: ['同比增速(%),净利增速显著高于营收增速体现利润弹性', financialViewDisclosure(view)].filter(Boolean).join('\n'),
|
||||||
},
|
},
|
||||||
legend: standardLegend(['非GAAP归母净利', '毛利润', '营业收入', '营业成本'], { itemGap: 34 }),
|
legend: standardLegend(['非GAAP归母净利', '毛利润', '营业收入', '营业成本'], { itemGap: 34 }),
|
||||||
grid: { left: 84, right: 56, top: LAYOUT.gridTop, bottom: 80 },
|
grid: { left: 84, right: 56, top: LAYOUT.gridTop, bottom: 80 },
|
||||||
@@ -60,7 +61,7 @@ export function ppmtGrowthChart(rows, { width, height }) {
|
|||||||
valueFormatter: (v) => `${v}%`,
|
valueFormatter: (v) => `${v}%`,
|
||||||
textStyle: { fontFamily: FONT },
|
textStyle: { fontFamily: FONT },
|
||||||
},
|
},
|
||||||
xAxis: { ...periodAxis, data: col(rows, 'period') },
|
xAxis: { ...standalonePeriodAxis, data: col(rows, 'period') },
|
||||||
yAxis: {
|
yAxis: {
|
||||||
...axisCommon,
|
...axisCommon,
|
||||||
type: 'value',
|
type: 'value',
|
||||||
@@ -105,7 +106,7 @@ export function ppmtGrowthChart(rows, { width, height }) {
|
|||||||
})),
|
})),
|
||||||
graphic: [
|
graphic: [
|
||||||
...watermarks(width, height),
|
...watermarks(width, height),
|
||||||
footnote('数据来源:泡泡玛特财报,2020H1/H2非GAAP净利用归母净利替代 | @思考的Joey'),
|
footnote('数据来源:泡泡玛特财报;2020H1/H2非GAAP净利用归母净利替代 | @思考的Joey'),
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
import { col } from '../csv.js';
|
||||||
|
import {
|
||||||
|
FINANCIAL_COLORS, PALETTE, FONT, axisCommon, standalonePeriodAxis, titleStyle, standardLegend,
|
||||||
|
financialViewLabel, barSeries, lineSeries, footnote, watermarks, LAYOUT,
|
||||||
|
} from '../theme.js';
|
||||||
|
|
||||||
|
/** 泡泡玛特存货余额与同报告期同比:存货为期末时点数。 */
|
||||||
|
export function inventoryBalanceChart(rows, { width, height, view = 'all' }) {
|
||||||
|
const yoy = col(rows, 'yoy').filter(Number.isFinite);
|
||||||
|
const yoyMin = Math.min(0, Math.floor(Math.min(...yoy) / 20) * 20);
|
||||||
|
const yoyMax = Math.max(50, Math.ceil(Math.max(...yoy) / 50) * 50);
|
||||||
|
const barWidth = view === 'all' ? 28.8 : 60.8;
|
||||||
|
|
||||||
|
return {
|
||||||
|
backgroundColor: '#fff',
|
||||||
|
title: {
|
||||||
|
...titleStyle,
|
||||||
|
text: `泡泡玛特存货余额及同比(${financialViewLabel(view)})`,
|
||||||
|
subtext: '柱状为报告期末存货余额;折线为同报告期同比(H1 对 H1、全年对全年)',
|
||||||
|
},
|
||||||
|
legend: standardLegend(['存货余额', '存货同比']),
|
||||||
|
grid: { left: 78, right: 82, top: LAYOUT.gridTop, bottom: 90 },
|
||||||
|
tooltip: {
|
||||||
|
trigger: 'axis',
|
||||||
|
axisPointer: { type: 'shadow' },
|
||||||
|
textStyle: { fontFamily: FONT },
|
||||||
|
valueFormatter: (value) => `${value.toFixed(1)}`,
|
||||||
|
},
|
||||||
|
xAxis: { ...standalonePeriodAxis, data: col(rows, 'period') },
|
||||||
|
yAxis: [
|
||||||
|
{
|
||||||
|
...axisCommon,
|
||||||
|
type: 'value',
|
||||||
|
name: '存货余额(亿元)',
|
||||||
|
nameLocation: 'middle',
|
||||||
|
nameGap: 52,
|
||||||
|
nameTextStyle: { color: PALETTE.grey, fontSize: 13, fontFamily: FONT },
|
||||||
|
min: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...axisCommon,
|
||||||
|
type: 'value',
|
||||||
|
name: '同比(%)',
|
||||||
|
nameLocation: 'middle',
|
||||||
|
nameGap: 58,
|
||||||
|
nameRotate: -90,
|
||||||
|
nameTextStyle: { color: PALETTE.grey, fontSize: 13, fontFamily: FONT },
|
||||||
|
min: yoyMin,
|
||||||
|
max: yoyMax,
|
||||||
|
interval: (yoyMax - yoyMin) / 5,
|
||||||
|
axisLabel: { ...axisCommon.axisLabel, formatter: '{value}%' },
|
||||||
|
splitLine: { show: false },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
...barSeries({
|
||||||
|
name: '存货余额',
|
||||||
|
data: col(rows, 'inventory'),
|
||||||
|
color: FINANCIAL_COLORS.investingCashFlow,
|
||||||
|
barWidth,
|
||||||
|
formatter: (point) => point.value.toFixed(1),
|
||||||
|
}),
|
||||||
|
emphasis: { focus: 'series' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...lineSeries({
|
||||||
|
name: '存货同比',
|
||||||
|
data: col(rows, 'yoy'),
|
||||||
|
color: FINANCIAL_COLORS.cashConversion,
|
||||||
|
formatter: (point) => `${point.value.toFixed(1)}%`,
|
||||||
|
}),
|
||||||
|
yAxisIndex: 1,
|
||||||
|
z: 10,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
graphic: [
|
||||||
|
...watermarks(width, height),
|
||||||
|
footnote('数据来源:泡泡玛特历年合并资产负债表;存货为报告期末时点数 | @思考的Joey'),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { col } from '../csv.js';
|
import { col } from '../csv.js';
|
||||||
import {
|
import {
|
||||||
PALETTE, FONT, axisCommon, categoryAxis, titleStyle, legendStyle,
|
PALETTE, FONT, axisCommon, categoryAxis, titleStyle, legendStyle,
|
||||||
|
financialViewDisclosure, financialViewLabel, shortStandalonePeriod,
|
||||||
lineSeries, footnote, watermarks, LAYOUT,
|
lineSeries, footnote, watermarks, LAYOUT,
|
||||||
} from '../theme.js';
|
} from '../theme.js';
|
||||||
|
|
||||||
@@ -8,7 +9,7 @@ import {
|
|||||||
* 泡泡玛特主要IP收入趋势图
|
* 泡泡玛特主要IP收入趋势图
|
||||||
* 多条折线展示各IP的收入变化
|
* 多条折线展示各IP的收入变化
|
||||||
*/
|
*/
|
||||||
export function ipChart(rows, { width, height }) {
|
export function ipChart(rows, { width, height, view = 'all' }) {
|
||||||
// 提取时期(从第一行的列名)
|
// 提取时期(从第一行的列名)
|
||||||
const periods = Object.keys(rows[0]).filter(k => k !== 'ip');
|
const periods = Object.keys(rows[0]).filter(k => k !== 'ip');
|
||||||
|
|
||||||
@@ -42,8 +43,8 @@ export function ipChart(rows, { width, height }) {
|
|||||||
backgroundColor: '#fff',
|
backgroundColor: '#fff',
|
||||||
title: {
|
title: {
|
||||||
...titleStyle,
|
...titleStyle,
|
||||||
text: '泡泡玛特主要IP收入趋势(2021H1-2025H2)',
|
text: `泡泡玛特主要IP收入趋势(${financialViewLabel(view)})`,
|
||||||
subtext: '单位:亿元',
|
subtext: ['单位:亿元;H2为单独半年', financialViewDisclosure(view)].filter(Boolean).join('\n'),
|
||||||
},
|
},
|
||||||
legend: {
|
legend: {
|
||||||
...legendStyle,
|
...legendStyle,
|
||||||
@@ -62,7 +63,7 @@ export function ipChart(rows, { width, height }) {
|
|||||||
data: periods,
|
data: periods,
|
||||||
axisLabel: {
|
axisLabel: {
|
||||||
...categoryAxis.axisLabel,
|
...categoryAxis.axisLabel,
|
||||||
formatter: (value) => String(value).replace(/^20/, ''),
|
formatter: shortStandalonePeriod,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
yAxis: {
|
yAxis: {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { col } from '../csv.js';
|
import { col } from '../csv.js';
|
||||||
import {
|
import {
|
||||||
PALETTE, FONT, axisCommon, categoryAxis, titleStyle, labelStyle,
|
PALETTE, FONT, axisCommon, standalonePeriodAxis, titleStyle, labelStyle,
|
||||||
|
financialViewDisclosure, financialViewLabel,
|
||||||
barSeries, lineSeries, standardLegend, footnote, watermarks, LAYOUT,
|
barSeries, lineSeries, standardLegend, footnote, watermarks, LAYOUT,
|
||||||
} from '../theme.js';
|
} from '../theme.js';
|
||||||
|
|
||||||
@@ -34,13 +35,13 @@ const BAR_COLOR = '#176B52'; // 墨翡翠
|
|||||||
const LINE_COLOR = '#7A3FE0'; // 鲜亮紫
|
const LINE_COLOR = '#7A3FE0'; // 鲜亮紫
|
||||||
const LINE_ON_BAR = '#DCC9FF'; // 压在翡翠柱面上时用的浅紫(对比度约 6:1)
|
const LINE_ON_BAR = '#DCC9FF'; // 压在翡翠柱面上时用的浅紫(对比度约 6:1)
|
||||||
|
|
||||||
const Y_MAX = 160; // 左轴上限
|
|
||||||
const YOY_MIN = -50; // 右轴下限
|
const YOY_MIN = -50; // 右轴下限
|
||||||
const YOY_MAX = 150; // 右轴上限
|
const YOY_MAX = 150; // 右轴上限
|
||||||
|
|
||||||
export function ppmtIpExMonstersChart(rows, { width, height }) {
|
export function ppmtIpExMonstersChart(rows, { width, height, view = 'all' }) {
|
||||||
const periods = col(rows, 'period');
|
const periods = col(rows, 'period');
|
||||||
const revenue = col(rows, 'revenue');
|
const revenue = col(rows, 'revenue');
|
||||||
|
const yMax = Math.max(...revenue) > 160 ? 300 : 160;
|
||||||
// 前两期无同比基数,CSV 里留空;parseCSV 会给出 '',统一转成 null
|
// 前两期无同比基数,CSV 里留空;parseCSV 会给出 '',统一转成 null
|
||||||
const yoy = col(rows, 'yoy').map((v) => (v === '' ? null : v));
|
const yoy = col(rows, 'yoy').map((v) => (v === '' ? null : v));
|
||||||
|
|
||||||
@@ -50,7 +51,7 @@ export function ppmtIpExMonstersChart(rows, { width, height }) {
|
|||||||
const labelGap = (8 + 13) / plotH; // labelStyle 的 distance + 字高,换成占比
|
const labelGap = (8 + 13) / plotH; // labelStyle 的 distance + 字高,换成占比
|
||||||
const yoyData = yoy.map((v, i) => {
|
const yoyData = yoy.map((v, i) => {
|
||||||
if (v === null) return null;
|
if (v === null) return null;
|
||||||
const barFrac = revenue[i] / Y_MAX;
|
const barFrac = revenue[i] / yMax;
|
||||||
const lineFrac = (v - YOY_MIN) / (YOY_MAX - YOY_MIN);
|
const lineFrac = (v - YOY_MIN) / (YOY_MAX - YOY_MIN);
|
||||||
// 标签整体(底边到顶边)都落在柱子高度以内 → 压在柱面上
|
// 标签整体(底边到顶边)都落在柱子高度以内 → 压在柱面上
|
||||||
const onBar = lineFrac + labelGap < barFrac;
|
const onBar = lineFrac + labelGap < barFrac;
|
||||||
@@ -61,8 +62,8 @@ export function ppmtIpExMonstersChart(rows, { width, height }) {
|
|||||||
backgroundColor: '#fff',
|
backgroundColor: '#fff',
|
||||||
title: {
|
title: {
|
||||||
...titleStyle,
|
...titleStyle,
|
||||||
text: '泡泡玛特除 THE MONSTERS 外的IP收入及同比(21H1-26H1)',
|
text: `泡泡玛特除 THE MONSTERS 外的IP收入及同比(${financialViewLabel(view)})`,
|
||||||
subtext: 'IP收入合计 − THE MONSTERS,单位:亿元;H2 为下半年单期,非全年累计',
|
subtext: ['IP收入合计 − THE MONSTERS,单位:亿元;H2为下半年单期,非全年累计', financialViewDisclosure(view)].filter(Boolean).join('\n'),
|
||||||
},
|
},
|
||||||
legend: standardLegend(['除MONSTERS外收入', '同比增速'], { itemGap: 32 }),
|
legend: standardLegend(['除MONSTERS外收入', '同比增速'], { itemGap: 32 }),
|
||||||
grid: { left: 78, right: 128, top: LAYOUT.gridTop, bottom: 90 },
|
grid: { left: 78, right: 128, top: LAYOUT.gridTop, bottom: 90 },
|
||||||
@@ -82,12 +83,7 @@ export function ppmtIpExMonstersChart(rows, { width, height }) {
|
|||||||
return out;
|
return out;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
xAxis: {
|
xAxis: { ...standalonePeriodAxis, data: periods },
|
||||||
...categoryAxis,
|
|
||||||
data: periods,
|
|
||||||
// 只去世纪前缀:这张表的 H2 是单半年,不能简写成「全年」
|
|
||||||
axisLabel: { ...categoryAxis.axisLabel, formatter: (v) => v.replace(/^20/, '') },
|
|
||||||
},
|
|
||||||
yAxis: [
|
yAxis: [
|
||||||
{
|
{
|
||||||
...axisCommon,
|
...axisCommon,
|
||||||
@@ -97,9 +93,8 @@ export function ppmtIpExMonstersChart(rows, { width, height }) {
|
|||||||
nameGap: 50,
|
nameGap: 50,
|
||||||
nameTextStyle: { color: PALETTE.grey, fontSize: 13, fontFamily: FONT },
|
nameTextStyle: { color: PALETTE.grey, fontSize: 13, fontFamily: FONT },
|
||||||
min: 0,
|
min: 0,
|
||||||
// 定死 160:留出顶部余量,最高柱(139.0)不会顶到绘图区上沿
|
max: yMax,
|
||||||
max: Y_MAX,
|
interval: yMax === 160 ? 20 : 50,
|
||||||
interval: 20,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'value',
|
type: 'value',
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
PALETTE, FONT, axisCommon, categoryAxis, titleStyle, LEGEND_ITEM_WIDTH,
|
PALETTE, FONT, axisCommon, categoryAxis, titleStyle,
|
||||||
barSeries, footnote, watermarks, LAYOUT,
|
financialViewDisclosure, financialViewLabel, shortStandalonePeriod,
|
||||||
|
standardLegend, barSeries, footnote, watermarks, LAYOUT,
|
||||||
} from '../theme.js';
|
} from '../theme.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -35,10 +36,7 @@ const COLORS = {
|
|||||||
'外采及其他': '#8C8C8C',
|
'外采及其他': '#8C8C8C',
|
||||||
};
|
};
|
||||||
|
|
||||||
const LEGEND_WIDTH = 160;
|
export function ppmtIpShareHistoryChart(rows, { width, height, view = 'all' }) {
|
||||||
const LEGEND_RIGHT = 20;
|
|
||||||
|
|
||||||
export function ppmtIpShareHistoryChart(rows, { width, height }) {
|
|
||||||
const periods = Object.keys(rows[0]).filter((k) => k !== 'ip');
|
const periods = Object.keys(rows[0]).filter((k) => k !== 'ip');
|
||||||
const names = rows.map((r) => r.ip);
|
const names = rows.map((r) => r.ip);
|
||||||
const totals = periods.map((p) => rows.reduce((s, r) => s + r[p], 0));
|
const totals = periods.map((p) => rows.reduce((s, r) => s + r[p], 0));
|
||||||
@@ -49,20 +47,11 @@ export function ppmtIpShareHistoryChart(rows, { width, height }) {
|
|||||||
backgroundColor: '#fff',
|
backgroundColor: '#fff',
|
||||||
title: {
|
title: {
|
||||||
...titleStyle,
|
...titleStyle,
|
||||||
text: '泡泡玛特各IP半年收入占比(21H1-26H1)',
|
text: `泡泡玛特各IP收入占比(${financialViewLabel(view)})`,
|
||||||
subtext: '占当期IP收入合计的比重;H2 为下半年单期,非全年累计',
|
subtext: ['占当期IP收入合计的比重;H2为下半年单期,非全年累计', financialViewDisclosure(view)].filter(Boolean).join('\n'),
|
||||||
},
|
|
||||||
legend: {
|
|
||||||
orient: 'vertical',
|
|
||||||
right: LEGEND_RIGHT,
|
|
||||||
width: LEGEND_WIDTH,
|
|
||||||
top: 'middle',
|
|
||||||
itemGap: 12,
|
|
||||||
itemWidth: LEGEND_ITEM_WIDTH,
|
|
||||||
itemHeight: 14,
|
|
||||||
data: names,
|
|
||||||
textStyle: { color: PALETTE.ink, fontSize: 12, fontFamily: FONT },
|
|
||||||
},
|
},
|
||||||
|
// 11 个系列较多:紧凑间距靠 ECharts 自动换行成多行
|
||||||
|
legend: standardLegend(names, { itemGap: 12, itemWidth: 14, itemHeight: 14 }),
|
||||||
tooltip: {
|
tooltip: {
|
||||||
trigger: 'axis',
|
trigger: 'axis',
|
||||||
axisPointer: { type: 'shadow' },
|
axisPointer: { type: 'shadow' },
|
||||||
@@ -79,17 +68,12 @@ export function ppmtIpShareHistoryChart(rows, { width, height }) {
|
|||||||
.join('<br/>');
|
.join('<br/>');
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
grid: {
|
grid: { left: 78, right: 82, top: LAYOUT.gridTop, bottom: 70 },
|
||||||
left: 78,
|
|
||||||
right: LEGEND_RIGHT + LEGEND_WIDTH + 20,
|
|
||||||
top: LAYOUT.gridTop,
|
|
||||||
bottom: 70,
|
|
||||||
},
|
|
||||||
xAxis: {
|
xAxis: {
|
||||||
...categoryAxis,
|
...categoryAxis,
|
||||||
data: periods,
|
data: periods,
|
||||||
// 只去世纪前缀:这张表的 H2 是单半年,不能简写成「全年」
|
// 只去世纪前缀:这张表的 H2 是单半年,不能简写成「全年」
|
||||||
axisLabel: { ...categoryAxis.axisLabel, formatter: (v) => v.replace(/^20/, '') },
|
axisLabel: { ...categoryAxis.axisLabel, formatter: shortStandalonePeriod },
|
||||||
},
|
},
|
||||||
yAxis: {
|
yAxis: {
|
||||||
...axisCommon,
|
...axisCommon,
|
||||||
@@ -113,7 +97,7 @@ export function ppmtIpShareHistoryChart(rows, { width, height }) {
|
|||||||
fontFamily: FONT,
|
fontFamily: FONT,
|
||||||
fontWeight: 500,
|
fontWeight: 500,
|
||||||
// 低于 4% 的段塞不进字,留空靠 tooltip 读数
|
// 低于 4% 的段塞不进字,留空靠 tooltip 读数
|
||||||
formatter: (p) => (p.value >= 4 ? p.value.toFixed(1) : ''),
|
formatter: (p) => (p.value >= 4 ? `${p.value.toFixed(1)}%` : ''),
|
||||||
},
|
},
|
||||||
emphasis: { focus: 'series' },
|
emphasis: { focus: 'series' },
|
||||||
})),
|
})),
|
||||||
|
|||||||
@@ -1,19 +1,20 @@
|
|||||||
import { col } from '../csv.js';
|
import { col } from '../csv.js';
|
||||||
import {
|
import {
|
||||||
PALETTE, FONT, axisCommon, periodAxis, titleStyle, legendStyle,
|
PALETTE, FINANCIAL_COLORS, FONT, axisCommon, standalonePeriodAxis, titleStyle, legendStyle,
|
||||||
lineSeries, footnote, watermarks, LAYOUT,
|
financialViewLabel,
|
||||||
|
lineDataWithSafeLabels, lineSeries, barSeries, footnote, watermarks, LAYOUT,
|
||||||
} from '../theme.js';
|
} from '../theme.js';
|
||||||
|
|
||||||
/** 泡泡玛特现金、负债与净营运资本:观察流动性与资金占用。 */
|
/** 泡泡玛特现金、负债与净营运资本:观察流动性与资金占用。 */
|
||||||
export function liquidityWorkingCapitalChart(rows, { width, height }) {
|
export function liquidityWorkingCapitalChart(rows, { width, height, view = 'all' }) {
|
||||||
const periods = col(rows, 'period');
|
const periods = col(rows, 'period');
|
||||||
|
|
||||||
return {
|
return {
|
||||||
backgroundColor: '#fff',
|
backgroundColor: '#fff',
|
||||||
title: {
|
title: {
|
||||||
...titleStyle,
|
...titleStyle,
|
||||||
text: '泡泡玛特现金、负债与净营运资本',
|
text: `泡泡玛特现金、负债与净营运资本(${financialViewLabel(view)})`,
|
||||||
subtext: '净营运资本 = 流动资产 − 流动负债;单位:亿元',
|
subtext: '净营运资本 = 流动资产 − 流动负债;单位:亿元;H2 为年末时点',
|
||||||
},
|
},
|
||||||
legend: {
|
legend: {
|
||||||
...legendStyle,
|
...legendStyle,
|
||||||
@@ -27,7 +28,7 @@ export function liquidityWorkingCapitalChart(rows, { width, height }) {
|
|||||||
textStyle: { fontFamily: FONT },
|
textStyle: { fontFamily: FONT },
|
||||||
valueFormatter: (value) => `${value.toFixed(1)} 亿元`,
|
valueFormatter: (value) => `${value.toFixed(1)} 亿元`,
|
||||||
},
|
},
|
||||||
xAxis: { ...periodAxis, data: periods },
|
xAxis: { ...standalonePeriodAxis, data: periods },
|
||||||
yAxis: {
|
yAxis: {
|
||||||
...axisCommon,
|
...axisCommon,
|
||||||
type: 'value',
|
type: 'value',
|
||||||
@@ -39,26 +40,35 @@ export function liquidityWorkingCapitalChart(rows, { width, height }) {
|
|||||||
},
|
},
|
||||||
series: [
|
series: [
|
||||||
{
|
{
|
||||||
|
...barSeries({
|
||||||
name: '货币资金',
|
name: '货币资金',
|
||||||
type: 'bar',
|
|
||||||
data: col(rows, 'cash'),
|
data: col(rows, 'cash'),
|
||||||
|
color: FINANCIAL_COLORS.investingCashFlow,
|
||||||
barWidth: 18,
|
barWidth: 18,
|
||||||
itemStyle: { color: PALETTE.blue },
|
formatter: (p) => p.value.toFixed(1),
|
||||||
|
}),
|
||||||
emphasis: { focus: 'series' },
|
emphasis: { focus: 'series' },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
...barSeries({
|
||||||
name: '负债合计',
|
name: '负债合计',
|
||||||
type: 'bar',
|
|
||||||
data: col(rows, 'total_liabilities'),
|
data: col(rows, 'total_liabilities'),
|
||||||
|
color: FINANCIAL_COLORS.financingCashFlow,
|
||||||
barWidth: 18,
|
barWidth: 18,
|
||||||
itemStyle: { color: PALETTE.orange },
|
formatter: (p) => p.value.toFixed(1),
|
||||||
|
}),
|
||||||
emphasis: { focus: 'series' },
|
emphasis: { focus: 'series' },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
...lineSeries({
|
...lineSeries({
|
||||||
name: '净营运资本',
|
name: '净营运资本',
|
||||||
data: col(rows, 'net_working_capital'),
|
data: lineDataWithSafeLabels(rows, {
|
||||||
color: PALETTE.red,
|
lineKey: 'net_working_capital',
|
||||||
|
barKeys: ['cash', 'total_liabilities'],
|
||||||
|
// 净营运资本接近货币资金时,标签置于柱体另一侧。
|
||||||
|
closePosition: 'bottom',
|
||||||
|
}),
|
||||||
|
color: FINANCIAL_COLORS.operatingCashFlow,
|
||||||
formatter: (p) => p.value.toFixed(1),
|
formatter: (p) => p.value.toFixed(1),
|
||||||
width: 3,
|
width: 3,
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { col } from '../csv.js';
|
import { col } from '../csv.js';
|
||||||
import {
|
import {
|
||||||
PALETTE, FONT, axisCommon, periodAxis, titleStyle, standardLegend,
|
PALETTE, FONT, axisCommon, standalonePeriodAxis, titleStyle, standardLegend,
|
||||||
|
financialViewDisclosure, financialViewLabel,
|
||||||
lineSeries, footnote, watermarks, LAYOUT,
|
lineSeries, footnote, watermarks, LAYOUT,
|
||||||
} from '../theme.js';
|
} from '../theme.js';
|
||||||
|
|
||||||
@@ -54,15 +55,15 @@ function labeledData(rows, key) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ppmtMarginsChart(rows, { width, height }) {
|
export function ppmtMarginsChart(rows, { width, height, view = 'all' }) {
|
||||||
const pct = (p) => `${p.value}%`;
|
const pct = (p) => `${p.value}%`;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
backgroundColor: '#fff',
|
backgroundColor: '#fff',
|
||||||
title: {
|
title: {
|
||||||
...titleStyle,
|
...titleStyle,
|
||||||
text: '泡泡玛特利润率与费用率拆解(2020H1-2026H1)',
|
text: `泡泡玛特利润率与费用率拆解(${financialViewLabel(view)})`,
|
||||||
subtext: '毛利率抬升叠加费用率下降,共同推动净利率走高(H1为半年,H2为全年累计)',
|
subtext: ['毛利率抬升叠加费用率下降,共同推动净利率走高', financialViewDisclosure(view)].filter(Boolean).join('\n'),
|
||||||
},
|
},
|
||||||
legend: standardLegend(SERIES.map((series) => series.name)),
|
legend: standardLegend(SERIES.map((series) => series.name)),
|
||||||
grid: { left: 78, right: 56, top: LAYOUT.gridTop, bottom: 80 },
|
grid: { left: 78, right: 56, top: LAYOUT.gridTop, bottom: 80 },
|
||||||
@@ -71,7 +72,7 @@ export function ppmtMarginsChart(rows, { width, height }) {
|
|||||||
valueFormatter: (v) => `${v}%`,
|
valueFormatter: (v) => `${v}%`,
|
||||||
textStyle: { fontFamily: FONT },
|
textStyle: { fontFamily: FONT },
|
||||||
},
|
},
|
||||||
xAxis: { ...periodAxis, data: col(rows, 'period') },
|
xAxis: { ...standalonePeriodAxis, data: col(rows, 'period') },
|
||||||
yAxis: {
|
yAxis: {
|
||||||
...axisCommon,
|
...axisCommon,
|
||||||
type: 'value',
|
type: 'value',
|
||||||
|
|||||||
+12
-11
@@ -1,6 +1,7 @@
|
|||||||
import { col } from '../csv.js';
|
import { col } from '../csv.js';
|
||||||
import {
|
import {
|
||||||
PALETTE, FONT, axisCommon, periodAxis, titleStyle, legendStyle,
|
PALETTE, FONT, axisCommon, standalonePeriodAxis, titleStyle, legendStyle,
|
||||||
|
financialViewDisclosure, financialViewLabel,
|
||||||
lineSeries, footnote, watermarks, LAYOUT,
|
lineSeries, footnote, watermarks, LAYOUT,
|
||||||
} from '../theme.js';
|
} from '../theme.js';
|
||||||
|
|
||||||
@@ -8,19 +9,19 @@ import {
|
|||||||
* 泡泡玛特盈利能力分析图
|
* 泡泡玛特盈利能力分析图
|
||||||
* 柱状图展示收入和利润,折线图展示毛利率
|
* 柱状图展示收入和利润,折线图展示毛利率
|
||||||
*/
|
*/
|
||||||
export function profitChart(rows, { width, height }) {
|
export function profitChart(rows, { width, height, view = 'all' }) {
|
||||||
const periods = col(rows, 'period').reverse(); // 按时间正序
|
const periods = col(rows, 'period');
|
||||||
const revenue = col(rows, 'revenue').reverse();
|
const revenue = col(rows, 'revenue');
|
||||||
const grossProfit = col(rows, 'gross_profit').reverse();
|
const grossProfit = col(rows, 'gross_profit');
|
||||||
const netProfit = col(rows, 'net_profit').reverse();
|
const netProfit = col(rows, 'net_profit');
|
||||||
const grossMargin = col(rows, 'gross_margin').reverse();
|
const grossMargin = col(rows, 'gross_margin');
|
||||||
|
|
||||||
return {
|
return {
|
||||||
backgroundColor: '#fff',
|
backgroundColor: '#fff',
|
||||||
title: {
|
title: {
|
||||||
...titleStyle,
|
...titleStyle,
|
||||||
text: '泡泡玛特盈利能力分析(2022H2-2026H1)',
|
text: `泡泡玛特盈利能力分析(${financialViewLabel(view)})`,
|
||||||
subtext: '营收、毛利、净利润及毛利率变化',
|
subtext: ['营收、毛利、净利润及毛利率变化', financialViewDisclosure(view)].filter(Boolean).join('\n'),
|
||||||
},
|
},
|
||||||
legend: {
|
legend: {
|
||||||
...legendStyle,
|
...legendStyle,
|
||||||
@@ -33,7 +34,7 @@ export function profitChart(rows, { width, height }) {
|
|||||||
axisPointer: { type: 'shadow' },
|
axisPointer: { type: 'shadow' },
|
||||||
textStyle: { fontFamily: FONT },
|
textStyle: { fontFamily: FONT },
|
||||||
},
|
},
|
||||||
xAxis: { ...periodAxis, data: periods },
|
xAxis: { ...standalonePeriodAxis, data: periods },
|
||||||
yAxis: [
|
yAxis: [
|
||||||
{
|
{
|
||||||
...axisCommon,
|
...axisCommon,
|
||||||
@@ -122,7 +123,7 @@ export function profitChart(rows, { width, height }) {
|
|||||||
],
|
],
|
||||||
graphic: [
|
graphic: [
|
||||||
...watermarks(width, height),
|
...watermarks(width, height),
|
||||||
footnote('数据来源:泡泡玛特财报(累计数据)| @思考的Joey'),
|
footnote('数据来源:泡泡玛特财报;H2为单独半年 | @思考的Joey'),
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,111 @@
|
|||||||
|
import {
|
||||||
|
PALETTE, FONT, axisCommon, categoryAxis, titleStyle, standardLegend,
|
||||||
|
financialViewDisclosure, financialViewLabel, standalonePeriodAxis,
|
||||||
|
barSeries, footnote, watermarks, LAYOUT,
|
||||||
|
} from '../theme.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 泡泡玛特分地区营收占比(2024H1 起)。
|
||||||
|
* 归一化堆叠柱状图:每根柱为当期营收合计 = 100%,看地区结构变化。
|
||||||
|
*
|
||||||
|
* 口径:中国含港澳台,海外 = 亚太 + 美洲 + 欧洲及其他(2024 年起的新披露口径)。
|
||||||
|
* H2 为单半年(当年披露全年 − 上半年还原),非全年累计,轴用 standalonePeriodAxis。
|
||||||
|
* 2024H1 的新口径分地区收入未直接披露,按 2025 年中报披露的分地区同比还原
|
||||||
|
* (2025H1 值 ÷ (1+同比)),还原后加总与当期总营收误差 < 0.1 亿。
|
||||||
|
*
|
||||||
|
* 地区配色与同 Tab 门店数量图(ppmt_stores_retail.js)保持一致。
|
||||||
|
*/
|
||||||
|
const REGIONS = [
|
||||||
|
{ key: 'china', name: '中国', color: '#3F7FBE' },
|
||||||
|
{ key: 'apac', name: '亚太', color: '#36A28F' },
|
||||||
|
{ key: 'americas', name: '美洲', color: '#E4A349' },
|
||||||
|
{ key: 'europe', name: '欧洲及其他', color: PALETTE.brown },
|
||||||
|
];
|
||||||
|
|
||||||
|
// 浅色底(美洲金)用深字,其余深色底用白字
|
||||||
|
const INSIDE_LABEL_COLORS = {
|
||||||
|
china: '#fff',
|
||||||
|
apac: '#fff',
|
||||||
|
americas: PALETTE.ink,
|
||||||
|
europe: '#fff',
|
||||||
|
};
|
||||||
|
|
||||||
|
// 绘图区像素高:画布高 − grid top 145 − grid bottom 100;轴固定 0-100%。
|
||||||
|
// 低于约 13px 的段放不下 11px 段内标签:是柱顶段的标到柱顶上方,
|
||||||
|
// 上方还有其它段的标到柱体右侧。
|
||||||
|
const MIN_INSIDE_PX = 13;
|
||||||
|
|
||||||
|
function segmentLabel(row, item, ri, totals, keyIndex, pxPerPct) {
|
||||||
|
const pct = ((row[item.key] || 0) / totals[ri]) * 100;
|
||||||
|
if (pct * pxPerPct >= MIN_INSIDE_PX) {
|
||||||
|
return { value: pct, label: { position: 'inside', color: INSIDE_LABEL_COLORS[item.key] } };
|
||||||
|
}
|
||||||
|
const hasSegmentAbove = REGIONS.slice(keyIndex + 1).some((r) => (row[r.key] || 0) > 0);
|
||||||
|
return {
|
||||||
|
value: pct,
|
||||||
|
label: hasSegmentAbove
|
||||||
|
? { position: 'right', color: PALETTE.ink, distance: 4, textBorderColor: '#fff', textBorderWidth: 2 }
|
||||||
|
: { position: 'top', color: PALETTE.ink, distance: 4 },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function regionShareChart(rows, { width, height, view = 'all' }) {
|
||||||
|
const periods = rows.map((row) => row.period);
|
||||||
|
const totals = rows.map((row) => REGIONS.reduce((s, item) => s + (row[item.key] || 0), 0));
|
||||||
|
const pxPerPct = (height - LAYOUT.gridTop - 100) / 100;
|
||||||
|
|
||||||
|
return {
|
||||||
|
backgroundColor: '#fff',
|
||||||
|
title: {
|
||||||
|
...titleStyle,
|
||||||
|
text: `泡泡玛特分地区营收占比(${financialViewLabel(view)})`,
|
||||||
|
subtext: ['归一化堆叠,每根柱为当期营收合计=100%;H2为下半年单期', financialViewDisclosure(view)].filter(Boolean).join('\n'),
|
||||||
|
},
|
||||||
|
legend: standardLegend(REGIONS.map((item) => item.name)),
|
||||||
|
tooltip: {
|
||||||
|
trigger: 'axis',
|
||||||
|
axisPointer: { type: 'shadow' },
|
||||||
|
textStyle: { fontFamily: FONT },
|
||||||
|
formatter: (ps) => {
|
||||||
|
const i = periods.indexOf(ps[0].axisValue);
|
||||||
|
const head = `<b>${ps[0].axisValue}</b> 合计 ${totals[i].toFixed(1)} 亿元<br/>`;
|
||||||
|
return head + ps
|
||||||
|
.slice()
|
||||||
|
.reverse()
|
||||||
|
.map((p) => `${p.marker}${p.seriesName}: ${p.value.toFixed(1)}%`
|
||||||
|
+ `(${((p.value / 100) * totals[i]).toFixed(1)} 亿元)`)
|
||||||
|
.join('<br/>');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
grid: { left: 78, right: 82, 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: REGIONS.map((item, keyIndex) => barSeries({
|
||||||
|
name: item.name,
|
||||||
|
data: rows.map((row, ri) => segmentLabel(row, item, ri, totals, keyIndex, pxPerPct)),
|
||||||
|
color: item.color,
|
||||||
|
barWidth: 40,
|
||||||
|
stack: 'region-share',
|
||||||
|
label: {
|
||||||
|
show: true,
|
||||||
|
position: 'inside',
|
||||||
|
fontSize: 11,
|
||||||
|
fontFamily: FONT,
|
||||||
|
fontWeight: 500,
|
||||||
|
formatter: (p) => `${p.value.toFixed(1)}%`,
|
||||||
|
},
|
||||||
|
emphasis: { focus: 'series' },
|
||||||
|
})),
|
||||||
|
graphic: [
|
||||||
|
...watermarks(width, height),
|
||||||
|
footnote('数据来源:泡泡玛特财报;中国含港澳台\n2024H1为按2025年中报披露的分地区同比还原;H2为单半年 | @思考的Joey'),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
import { col } from '../csv.js';
|
||||||
|
import {
|
||||||
|
PALETTE, FONT, axisCommon, standalonePeriodAxis, titleStyle, standardLegend,
|
||||||
|
financialViewLabel, barSeries, lineSeries, footnote, watermarks, LAYOUT,
|
||||||
|
} from '../theme.js';
|
||||||
|
|
||||||
|
const REGIONS = [
|
||||||
|
{ key: 'china', name: '中国(含港澳台)', color: '#3F7FBE' },
|
||||||
|
{ key: 'apac', name: '亚太', color: '#36A28F' },
|
||||||
|
{ key: 'americas', name: '美洲', color: '#E4A349' },
|
||||||
|
{ key: 'europe_other', name: '欧洲及其他', color: PALETTE.brown },
|
||||||
|
];
|
||||||
|
|
||||||
|
function yoyByPeriod(rows, key) {
|
||||||
|
const values = new Map(rows.map((row) => [row.period, row[key]]));
|
||||||
|
return rows.map((row) => {
|
||||||
|
const year = Number(row.period.slice(0, 4));
|
||||||
|
const prior = values.get(`${year - 1}${row.period.slice(4)}`);
|
||||||
|
return Number.isFinite(row[key]) && Number.isFinite(prior) && prior !== 0
|
||||||
|
? (row[key] / prior - 1) * 100
|
||||||
|
: null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 各地区零售店单店收入:收入 ÷ 期初、期末平均零售店数。 */
|
||||||
|
export function retailStoreRevenueChart(rows, { width, height, view = 'all' }) {
|
||||||
|
const values = REGIONS.flatMap((region) => col(rows, `${region.key}_per_store`)).filter(Number.isFinite);
|
||||||
|
const yMax = Math.ceil(Math.max(...values) / 1000) * 1000;
|
||||||
|
const yoySeries = REGIONS.map((region) => ({ ...region, data: yoyByPeriod(rows, `${region.key}_per_store`) }));
|
||||||
|
const yoyValues = yoySeries.flatMap((region) => region.data).filter(Number.isFinite);
|
||||||
|
const yoyMin = Math.min(0, Math.floor(Math.min(...yoyValues) / 50) * 50);
|
||||||
|
const yoyMax = Math.max(50, Math.ceil(Math.max(...yoyValues) / 50) * 50);
|
||||||
|
const legendItems = REGIONS.flatMap((region) => [region.name, `${region.name}同比`]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
backgroundColor: '#fff',
|
||||||
|
title: {
|
||||||
|
...titleStyle,
|
||||||
|
text: `泡泡玛特各地区零售店单店收入(${financialViewLabel(view)})`,
|
||||||
|
subtext: '柱状为单店收入;同色虚线为单店收入同比;H2为下半年单期',
|
||||||
|
},
|
||||||
|
legend: standardLegend(legendItems, { itemGap: 18 }),
|
||||||
|
grid: { left: 78, right: 42, top: LAYOUT.gridTop, bottom: 90 },
|
||||||
|
tooltip: {
|
||||||
|
trigger: 'axis',
|
||||||
|
axisPointer: { type: 'shadow' },
|
||||||
|
textStyle: { fontFamily: FONT },
|
||||||
|
formatter: (params) => `<b>${params[0].axisValue}</b><br/>${params
|
||||||
|
.filter((item) => Number.isFinite(item.value))
|
||||||
|
.map((item) => `${item.marker}${item.seriesName}: ${Number(item.value).toFixed(1)}${item.seriesName.endsWith('同比') ? '%' : ' 万元/店'}`)
|
||||||
|
.join('<br/>')}`,
|
||||||
|
},
|
||||||
|
xAxis: { ...standalonePeriodAxis, data: col(rows, 'period') },
|
||||||
|
yAxis: [
|
||||||
|
{
|
||||||
|
...axisCommon,
|
||||||
|
type: 'value',
|
||||||
|
name: '单店收入(万元/店)',
|
||||||
|
nameLocation: 'middle',
|
||||||
|
nameGap: 58,
|
||||||
|
nameTextStyle: { color: PALETTE.grey, fontSize: 13, fontFamily: FONT },
|
||||||
|
min: 0,
|
||||||
|
max: yMax,
|
||||||
|
interval: yMax / 5,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...axisCommon,
|
||||||
|
type: 'value',
|
||||||
|
name: '单店收入同比(%)',
|
||||||
|
nameLocation: 'middle',
|
||||||
|
nameGap: 60,
|
||||||
|
nameRotate: -90,
|
||||||
|
nameTextStyle: { color: PALETTE.grey, fontSize: 13, fontFamily: FONT },
|
||||||
|
min: yoyMin,
|
||||||
|
max: yoyMax,
|
||||||
|
interval: 50,
|
||||||
|
axisLabel: { ...axisCommon.axisLabel, formatter: '{value}%' },
|
||||||
|
splitLine: { show: false },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
series: [
|
||||||
|
...REGIONS.map((region) => ({
|
||||||
|
...barSeries({
|
||||||
|
name: region.name,
|
||||||
|
data: col(rows, `${region.key}_per_store`),
|
||||||
|
color: region.color,
|
||||||
|
barWidth: view === 'all' ? 17 : 34,
|
||||||
|
formatter: (point) => point.value.toFixed(0),
|
||||||
|
}),
|
||||||
|
emphasis: { focus: 'series' },
|
||||||
|
})),
|
||||||
|
...yoySeries.map((region) => ({
|
||||||
|
...lineSeries({
|
||||||
|
name: `${region.name}同比`,
|
||||||
|
data: region.data,
|
||||||
|
color: region.color,
|
||||||
|
width: 2,
|
||||||
|
formatter: (point) => `${point.value.toFixed(0)}%`,
|
||||||
|
}),
|
||||||
|
yAxisIndex: 1,
|
||||||
|
lineStyle: { color: region.color, width: 2, type: 'dashed' },
|
||||||
|
z: 10,
|
||||||
|
})),
|
||||||
|
],
|
||||||
|
graphic: [
|
||||||
|
...watermarks(width, height),
|
||||||
|
footnote('数据来源:泡泡玛特历年公告;收入仅含“零售店”渠道;H2收入=全年−H1\n2024年海外各区域缺少期初店数,相关单店收入留空 | @思考的Joey'),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
import { col } from '../csv.js';
|
||||||
|
import {
|
||||||
|
PALETTE, FONT, axisCommon, standalonePeriodAxis, titleStyle, standardLegend,
|
||||||
|
financialViewLabel, barSeries, lineSeries, footnote, watermarks, LAYOUT,
|
||||||
|
} from '../theme.js';
|
||||||
|
|
||||||
|
const REGIONS = [
|
||||||
|
{ key: 'china', name: '中国(含港澳台)', color: '#71AD81' },
|
||||||
|
{ key: 'overseas', name: '海外', color: PALETTE.brown },
|
||||||
|
];
|
||||||
|
|
||||||
|
function yoyByPeriod(rows, key) {
|
||||||
|
const values = new Map(rows.map((row) => [row.period, row[key]]));
|
||||||
|
return rows.map((row) => {
|
||||||
|
const year = Number(row.period.slice(0, 4));
|
||||||
|
const prior = values.get(`${year - 1}${row.period.slice(4)}`);
|
||||||
|
return Number.isFinite(row[key]) && Number.isFinite(prior) && prior !== 0
|
||||||
|
? (row[key] / prior - 1) * 100
|
||||||
|
: null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 中国与海外机器人商店单店收入:收入 ÷ 期初、期末平均机器人商店数。 */
|
||||||
|
export function robotStoreRevenueChart(rows, { width, height, view = 'all' }) {
|
||||||
|
const values = REGIONS.flatMap((region) => col(rows, `${region.key}_per_store`)).filter(Number.isFinite);
|
||||||
|
const yMax = Math.ceil(Math.max(...values) / 50) * 50;
|
||||||
|
const yoySeries = REGIONS.map((region) => ({ ...region, data: yoyByPeriod(rows, `${region.key}_per_store`) }));
|
||||||
|
const yoyValues = yoySeries.flatMap((region) => region.data).filter(Number.isFinite);
|
||||||
|
const yoyMin = Math.min(0, Math.floor(Math.min(...yoyValues) / 50) * 50);
|
||||||
|
const yoyMax = Math.max(50, Math.ceil(Math.max(...yoyValues) / 50) * 50);
|
||||||
|
const legendItems = REGIONS.flatMap((region) => [region.name, `${region.name}同比`]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
backgroundColor: '#fff',
|
||||||
|
title: {
|
||||||
|
...titleStyle,
|
||||||
|
text: `泡泡玛特机器人商店单店收入(${financialViewLabel(view)})`,
|
||||||
|
subtext: '柱状为单店收入;同色虚线为单店收入同比;H2为下半年单期',
|
||||||
|
},
|
||||||
|
legend: standardLegend(legendItems, { itemGap: 18 }),
|
||||||
|
grid: { left: 78, right: 42, top: LAYOUT.gridTop, bottom: 90 },
|
||||||
|
tooltip: {
|
||||||
|
trigger: 'axis',
|
||||||
|
axisPointer: { type: 'shadow' },
|
||||||
|
textStyle: { fontFamily: FONT },
|
||||||
|
formatter: (params) => `<b>${params[0].axisValue}</b><br/>${params
|
||||||
|
.filter((item) => Number.isFinite(item.value))
|
||||||
|
.map((item) => `${item.marker}${item.seriesName}: ${Number(item.value).toFixed(1)}${item.seriesName.endsWith('同比') ? '%' : ' 万元/店'}`)
|
||||||
|
.join('<br/>')}`,
|
||||||
|
},
|
||||||
|
xAxis: { ...standalonePeriodAxis, data: col(rows, 'period') },
|
||||||
|
yAxis: [
|
||||||
|
{
|
||||||
|
...axisCommon,
|
||||||
|
type: 'value',
|
||||||
|
name: '单店收入(万元/店)',
|
||||||
|
nameLocation: 'middle',
|
||||||
|
nameGap: 58,
|
||||||
|
nameTextStyle: { color: PALETTE.grey, fontSize: 13, fontFamily: FONT },
|
||||||
|
min: 0,
|
||||||
|
max: yMax,
|
||||||
|
interval: yMax / 5,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...axisCommon,
|
||||||
|
type: 'value',
|
||||||
|
name: '单店收入同比(%)',
|
||||||
|
nameLocation: 'middle',
|
||||||
|
nameGap: 60,
|
||||||
|
nameRotate: -90,
|
||||||
|
nameTextStyle: { color: PALETTE.grey, fontSize: 13, fontFamily: FONT },
|
||||||
|
min: yoyMin,
|
||||||
|
max: yoyMax,
|
||||||
|
interval: 50,
|
||||||
|
axisLabel: { ...axisCommon.axisLabel, formatter: '{value}%' },
|
||||||
|
splitLine: { show: false },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
series: [
|
||||||
|
...REGIONS.map((region) => ({
|
||||||
|
...barSeries({
|
||||||
|
name: region.name,
|
||||||
|
data: col(rows, `${region.key}_per_store`),
|
||||||
|
color: region.color,
|
||||||
|
barWidth: view === 'all' ? 28 : 52,
|
||||||
|
formatter: (point) => point.value.toFixed(0),
|
||||||
|
}),
|
||||||
|
emphasis: { focus: 'series' },
|
||||||
|
})),
|
||||||
|
...yoySeries.map((region) => ({
|
||||||
|
...lineSeries({
|
||||||
|
name: `${region.name}同比`,
|
||||||
|
data: region.data,
|
||||||
|
color: region.color,
|
||||||
|
width: 2,
|
||||||
|
formatter: (point) => `${point.value.toFixed(0)}%`,
|
||||||
|
}),
|
||||||
|
yAxisIndex: 1,
|
||||||
|
lineStyle: { color: region.color, width: 2, type: 'dashed' },
|
||||||
|
z: 10,
|
||||||
|
})),
|
||||||
|
],
|
||||||
|
graphic: [
|
||||||
|
...watermarks(width, height),
|
||||||
|
footnote('数据来源:泡泡玛特历年公告;海外收入=亚太+美洲+欧洲及其他的机器人商店收入;H2收入=全年−H1\n2024H1海外期末机器人商店数未披露,相关单店收入留空 | @思考的Joey'),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
import { col } from '../csv.js';
|
||||||
|
import {
|
||||||
|
PALETTE, FONT, axisCommon, standalonePeriodAxis, titleStyle, standardLegend,
|
||||||
|
financialViewLabel, lineSeries, footnote, watermarks, LAYOUT,
|
||||||
|
} from '../theme.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 泡泡玛特分地区零售门店数量与同比增速。
|
||||||
|
* 堆叠柱为各期末门店数,折线为全球合计数量的同比。
|
||||||
|
* 口径:2023年及以前"海外合计"含港澳台;2024年起港澳台单列并计入中国,
|
||||||
|
* 海外合计 = 亚太 + 美洲 + 欧洲。两段口径不可直接跨 2023/2024 对比海外。
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 门店数是计数而非金额,保留整数(CLAUDE.md 一位小数规则针对金额/比率)。
|
||||||
|
const SEGMENT_COLORS = {
|
||||||
|
mainland: '#3F7FBE',
|
||||||
|
hkmtw: '#AECBE8',
|
||||||
|
apac: '#36A28F',
|
||||||
|
americas: '#E4A349',
|
||||||
|
europe: PALETTE.brown,
|
||||||
|
overseas: '#C2726B',
|
||||||
|
};
|
||||||
|
|
||||||
|
// 段内标签文字色:浅色底(港澳台、美洲)用深字,深色底用白字
|
||||||
|
const INSIDE_LABEL_COLORS = {
|
||||||
|
mainland: '#fff',
|
||||||
|
hkmtw: PALETTE.ink,
|
||||||
|
apac: '#fff',
|
||||||
|
americas: PALETTE.ink,
|
||||||
|
europe: '#fff',
|
||||||
|
overseas: '#fff',
|
||||||
|
};
|
||||||
|
|
||||||
|
const regions = [
|
||||||
|
{ key: 'mainland', name: '中国内地' },
|
||||||
|
{ key: 'hkmtw', name: '港澳台' },
|
||||||
|
{ key: 'apac', name: '亚太' },
|
||||||
|
{ key: 'americas', name: '美洲' },
|
||||||
|
{ key: 'europe', name: '欧洲' },
|
||||||
|
{ key: 'overseas', name: '海外合计(含港澳台)' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const num = (value) => (Number.isFinite(value) ? value : null);
|
||||||
|
|
||||||
|
// 2024 起披露口径变为港澳台单列 + 亚太/美洲/欧洲分列;此前只有含港澳台的海外合计。
|
||||||
|
// 同一根柱内两种披露结构互斥出现,堆叠总和始终等于全球合计。
|
||||||
|
function stackValue(row, key) {
|
||||||
|
const split = Number.isFinite(row.apac);
|
||||||
|
if (key === 'overseas') return split ? null : num(row.overseas);
|
||||||
|
if (!split && key !== 'mainland') return null;
|
||||||
|
return num(row[key]);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function storesRetailChart(rows, { width, height, view = 'all' }) {
|
||||||
|
return {
|
||||||
|
backgroundColor: '#fff',
|
||||||
|
title: {
|
||||||
|
...titleStyle,
|
||||||
|
text: `泡泡玛特分地区零售门店数量与同比增速(${financialViewLabel(view)})`,
|
||||||
|
subtext: '柱状为各期末零售门店数(堆叠);折线为全球合计门店数量同比',
|
||||||
|
},
|
||||||
|
legend: standardLegend([...regions.map((item) => item.name), '全球合计同比']),
|
||||||
|
grid: { left: 78, right: 82, top: LAYOUT.gridTop, bottom: 100 },
|
||||||
|
tooltip: {
|
||||||
|
trigger: 'axis',
|
||||||
|
axisPointer: { type: 'shadow' },
|
||||||
|
textStyle: { fontFamily: FONT },
|
||||||
|
},
|
||||||
|
xAxis: { ...standalonePeriodAxis, data: col(rows, 'period') },
|
||||||
|
yAxis: [
|
||||||
|
{
|
||||||
|
...axisCommon,
|
||||||
|
type: 'value',
|
||||||
|
name: '零售门店数量(家)',
|
||||||
|
nameLocation: 'middle',
|
||||||
|
nameGap: 52,
|
||||||
|
nameTextStyle: { color: PALETTE.grey, fontSize: 13, fontFamily: FONT },
|
||||||
|
min: 0,
|
||||||
|
max: 700,
|
||||||
|
interval: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...axisCommon,
|
||||||
|
type: 'value',
|
||||||
|
name: '全球合计同比(%)',
|
||||||
|
nameLocation: 'middle',
|
||||||
|
nameGap: 60,
|
||||||
|
nameRotate: -90,
|
||||||
|
nameTextStyle: { color: PALETTE.grey, fontSize: 13, fontFamily: FONT },
|
||||||
|
min: 0,
|
||||||
|
max: 100,
|
||||||
|
interval: 20,
|
||||||
|
axisLabel: { ...axisCommon.axisLabel, formatter: '{value}%' },
|
||||||
|
splitLine: { show: false },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
series: [
|
||||||
|
...regions.map((item) => ({
|
||||||
|
name: item.name,
|
||||||
|
type: 'bar',
|
||||||
|
stack: 'stores-retail',
|
||||||
|
data: rows.map((row) => stackValue(row, item.key)),
|
||||||
|
itemStyle: { color: SEGMENT_COLORS[item.key] },
|
||||||
|
// 每段标注门店数:浅色段用深字、深色段用白字,保证段内可读
|
||||||
|
label: {
|
||||||
|
show: true,
|
||||||
|
position: 'inside',
|
||||||
|
fontSize: 10,
|
||||||
|
fontWeight: 500,
|
||||||
|
fontFamily: FONT,
|
||||||
|
color: INSIDE_LABEL_COLORS[item.key],
|
||||||
|
overflow: 'none',
|
||||||
|
},
|
||||||
|
// 相邻矮段(如 2024H1 美洲/欧洲)标签贴得近时沿 y 向微移避让
|
||||||
|
labelLayout: { moveOverlap: 'shiftY' },
|
||||||
|
emphasis: { focus: 'series' },
|
||||||
|
})),
|
||||||
|
{
|
||||||
|
...lineSeries({
|
||||||
|
name: '全球合计同比',
|
||||||
|
data: col(rows, 'global_yoy'),
|
||||||
|
color: '#8256B2',
|
||||||
|
formatter: (p) => `${p.value.toFixed(1)}%`,
|
||||||
|
}),
|
||||||
|
yAxisIndex: 1,
|
||||||
|
z: 10,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
graphic: [
|
||||||
|
...watermarks(width, height),
|
||||||
|
footnote('数据来源:泡泡玛特历年公告;门店数为各期末时点数\n2023年及以前海外合计含港澳台;2024年起港澳台单列,海外=亚太+美洲+欧洲 | @思考的Joey'),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
import { col } from '../csv.js';
|
||||||
|
import {
|
||||||
|
PALETTE, FONT, axisCommon, standalonePeriodAxis, titleStyle, standardLegend,
|
||||||
|
financialViewLabel, barSeries, footnote, watermarks, LAYOUT,
|
||||||
|
} from '../theme.js';
|
||||||
|
|
||||||
|
/** 泡泡玛特分地区零售门店占比:每根柱标准化为各期末全球门店数的 100%。 */
|
||||||
|
const REGIONS = [
|
||||||
|
{ key: 'china', name: '中国(含港澳台)', color: '#3F7FBE', labelColor: '#fff' },
|
||||||
|
{ key: 'apac', name: '亚太', color: '#36A28F', labelColor: '#fff' },
|
||||||
|
{ key: 'americas', name: '美洲', color: '#E4A349', labelColor: PALETTE.ink },
|
||||||
|
{ key: 'europe', name: '欧洲', color: PALETTE.brown, labelColor: '#fff' },
|
||||||
|
{ key: 'other', name: '其他', color: PALETTE.sand, labelColor: PALETTE.ink },
|
||||||
|
];
|
||||||
|
|
||||||
|
function sharePoint(value, labelColor) {
|
||||||
|
if (!Number.isFinite(value) || value === 0) return null;
|
||||||
|
return {
|
||||||
|
value,
|
||||||
|
label: value >= 5
|
||||||
|
? { position: 'inside', color: labelColor }
|
||||||
|
: { show: false },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function storesRetailShareChart(rows, { width, height, view = 'all' }) {
|
||||||
|
const periods = col(rows, 'period');
|
||||||
|
const barWidth = view === 'all' ? 48 : 80;
|
||||||
|
|
||||||
|
return {
|
||||||
|
backgroundColor: '#fff',
|
||||||
|
title: {
|
||||||
|
...titleStyle,
|
||||||
|
text: `泡泡玛特分地区零售门店数量占比(${financialViewLabel(view)})`,
|
||||||
|
subtext: '归一化堆叠:每根柱为当期末全球零售门店数的 100%',
|
||||||
|
},
|
||||||
|
legend: standardLegend(REGIONS.map((item) => item.name), { itemGap: 20 }),
|
||||||
|
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.global} 家<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: REGIONS.map((item) => barSeries({
|
||||||
|
name: item.name,
|
||||||
|
data: rows.map((row) => sharePoint(row[item.key], item.labelColor)),
|
||||||
|
color: item.color,
|
||||||
|
barWidth,
|
||||||
|
stack: 'stores-retail-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('数据来源:泡泡玛特历年公告;门店数为各报告期末时点数\n中国=中国内地+港澳台;其他=全球合计−中国−亚太−美洲−欧洲 | @思考的Joey'),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,144 @@
|
|||||||
|
import { col } from '../csv.js';
|
||||||
|
import {
|
||||||
|
CHANNEL_COLORS, PALETTE, FONT, axisCommon, standalonePeriodAxis, titleStyle,
|
||||||
|
standardLegend, financialViewLabel, lineSeries, footnote, watermarks, LAYOUT,
|
||||||
|
} from '../theme.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 泡泡玛特分地区机器人商店数量与同比增速。
|
||||||
|
* 堆叠柱为各期末机器人数,折线为全球合计数量的同比。
|
||||||
|
* 口径:2023年及以前"海外合计"含港澳台;2024年起港澳台单列并计入中国。
|
||||||
|
* 机器人商店仅披露内地/港澳台/海外三层,无亚太/美洲/欧洲分列。
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 门店数是计数而非金额,保留整数。中国内地沿用渠道语义色(机器人商店=绿)。
|
||||||
|
const SEGMENT_COLORS = {
|
||||||
|
mainland: CHANNEL_COLORS.robotStore,
|
||||||
|
hkmtw: '#AECBE8',
|
||||||
|
overseas: '#C2726B',
|
||||||
|
};
|
||||||
|
|
||||||
|
const regions = [
|
||||||
|
{ key: 'mainland', name: '中国内地' },
|
||||||
|
{ key: 'hkmtw', name: '港澳台' },
|
||||||
|
{ key: 'overseas', name: '海外合计(含港澳台)' },
|
||||||
|
];
|
||||||
|
|
||||||
|
// 段内标签文字色:浅色底(港澳台)用深字,深色底(内地绿、海外玫红)用白字
|
||||||
|
const INSIDE_LABEL_COLORS = {
|
||||||
|
mainland: '#fff',
|
||||||
|
hkmtw: PALETTE.ink,
|
||||||
|
overseas: '#fff',
|
||||||
|
};
|
||||||
|
|
||||||
|
const num = (value) => (Number.isFinite(value) ? value : null);
|
||||||
|
|
||||||
|
// 绘图区高约 435px(grid top 145 / bottom 100,画布 680),轴上限 3000。
|
||||||
|
// 低于约 13px 的段放不下段内 10px 标签:柱顶段标到柱顶上方,
|
||||||
|
// 上方还有其它段的标到柱体右侧,避免与上段段内标签互相挤压。
|
||||||
|
const PX_PER_UNIT = 435 / 3000;
|
||||||
|
const MIN_INSIDE_PX = 13;
|
||||||
|
|
||||||
|
function segmentData(row, key) {
|
||||||
|
const value = num(row[key]);
|
||||||
|
if (value === null) return null;
|
||||||
|
if (value * PX_PER_UNIT >= MIN_INSIDE_PX) {
|
||||||
|
return { value, label: { position: 'inside', color: INSIDE_LABEL_COLORS[key] } };
|
||||||
|
}
|
||||||
|
const keyIndex = regions.findIndex((item) => item.key === key);
|
||||||
|
const hasSegmentAbove = regions
|
||||||
|
.slice(keyIndex + 1)
|
||||||
|
.some((item) => num(row[item.key]) !== null);
|
||||||
|
return {
|
||||||
|
value,
|
||||||
|
label: hasSegmentAbove
|
||||||
|
? { position: 'right', color: PALETTE.ink, distance: 4 }
|
||||||
|
: { position: 'top', color: PALETTE.ink },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function storesRobotChart(rows, { width, height, view = 'all' }) {
|
||||||
|
return {
|
||||||
|
backgroundColor: '#fff',
|
||||||
|
title: {
|
||||||
|
...titleStyle,
|
||||||
|
text: `泡泡玛特分地区机器人商店数量与同比增速(${financialViewLabel(view)})`,
|
||||||
|
subtext: '柱状为各期末机器人商店数(堆叠);折线为全球合计数量同比',
|
||||||
|
},
|
||||||
|
legend: standardLegend([...regions.map((item) => item.name), '全球合计同比']),
|
||||||
|
grid: { left: 78, right: 82, top: LAYOUT.gridTop, bottom: 100 },
|
||||||
|
tooltip: {
|
||||||
|
trigger: 'axis',
|
||||||
|
axisPointer: { type: 'shadow' },
|
||||||
|
textStyle: { fontFamily: FONT },
|
||||||
|
},
|
||||||
|
xAxis: { ...standalonePeriodAxis, data: col(rows, 'period') },
|
||||||
|
yAxis: [
|
||||||
|
{
|
||||||
|
...axisCommon,
|
||||||
|
type: 'value',
|
||||||
|
name: '机器人商店数量(台)',
|
||||||
|
nameLocation: 'middle',
|
||||||
|
nameGap: 52,
|
||||||
|
nameTextStyle: { color: PALETTE.grey, fontSize: 13, fontFamily: FONT },
|
||||||
|
min: 0,
|
||||||
|
max: 3000,
|
||||||
|
interval: 500,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...axisCommon,
|
||||||
|
type: 'value',
|
||||||
|
name: '全球合计同比(%)',
|
||||||
|
nameLocation: 'middle',
|
||||||
|
nameGap: 60,
|
||||||
|
nameRotate: -90,
|
||||||
|
nameTextStyle: { color: PALETTE.grey, fontSize: 13, fontFamily: FONT },
|
||||||
|
min: 0,
|
||||||
|
max: 600,
|
||||||
|
interval: 100,
|
||||||
|
axisLabel: { ...axisCommon.axisLabel, formatter: '{value}%' },
|
||||||
|
splitLine: { show: false },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
series: [
|
||||||
|
...regions.map((item) => ({
|
||||||
|
name: item.name,
|
||||||
|
type: 'bar',
|
||||||
|
stack: 'stores-robot',
|
||||||
|
data: rows.map((row) => segmentData(row, item.key)),
|
||||||
|
itemStyle: { color: SEGMENT_COLORS[item.key] },
|
||||||
|
label: {
|
||||||
|
show: true,
|
||||||
|
fontSize: 10,
|
||||||
|
fontWeight: 500,
|
||||||
|
fontFamily: FONT,
|
||||||
|
overflow: 'none',
|
||||||
|
},
|
||||||
|
// 相邻矮段标签贴近时沿 y 向微移避让
|
||||||
|
labelLayout: { moveOverlap: 'shiftY' },
|
||||||
|
emphasis: { focus: 'series' },
|
||||||
|
})),
|
||||||
|
{
|
||||||
|
...lineSeries({
|
||||||
|
name: '全球合计同比',
|
||||||
|
data: col(rows, 'global_yoy'),
|
||||||
|
color: '#8256B2',
|
||||||
|
formatter: (p) => `${p.value.toFixed(1)}%`,
|
||||||
|
}),
|
||||||
|
yAxisIndex: 1,
|
||||||
|
z: 10,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
graphic: [
|
||||||
|
...watermarks(width, height),
|
||||||
|
footnote(
|
||||||
|
[
|
||||||
|
'数据来源:泡泡玛特历年公告;门店数为各期末时点数',
|
||||||
|
view === 'annual'
|
||||||
|
? '海外合计2023年及以前含港澳台、其后不含 | @思考的Joey'
|
||||||
|
: '海外合计2023年及以前含港澳台、其后不含;2024H1海外数量未披露 | @思考的Joey',
|
||||||
|
].join('\n'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,26 +1,44 @@
|
|||||||
import { col } from '../csv.js';
|
import { col } from '../csv.js';
|
||||||
import {
|
import {
|
||||||
PALETTE, FONT, axisCommon, periodAxis, titleStyle, legendStyle,
|
PALETTE, FINANCIAL_COLORS, FONT, axisCommon, periodAxis, titleStyle, legendStyle,
|
||||||
lineSeries, footnote, watermarks, LAYOUT,
|
lineSeries, footnote, watermarks, LAYOUT,
|
||||||
} from '../theme.js';
|
} from '../theme.js';
|
||||||
|
|
||||||
/** 泡泡玛特营运资本效率:存货、应收、应付与现金转换周期。 */
|
/** 泡泡玛特营运资本效率:存货、应收、应付与现金转换周期。 */
|
||||||
export function workingCapitalDaysChart(rows, { width, height }) {
|
const SERIES = [
|
||||||
const series = [
|
{ key: 'inventory_days', name: '存货周转天数', color: FINANCIAL_COLORS.financingCashFlow, width: 2.4 },
|
||||||
{ key: 'inventory_days', name: '存货周转天数', color: PALETTE.orange, width: 2.4 },
|
{ key: 'receivable_days', name: '应收周转天数', color: FINANCIAL_COLORS.investingCashFlow, width: 2.2 },
|
||||||
{ key: 'receivable_days', name: '应收周转天数', color: PALETTE.blue, width: 2.2 },
|
{ key: 'payable_days', name: '应付周转天数', color: FINANCIAL_COLORS.cashConversion, width: 2.2 },
|
||||||
{ key: 'payable_days', name: '应付周转天数', color: PALETTE.purple, width: 2.2 },
|
{ key: 'cash_conversion_cycle', name: '现金转换周期', color: FINANCIAL_COLORS.netCashIncrease, width: 3.2 },
|
||||||
{ key: 'cash_conversion_cycle', name: '现金转换周期', color: PALETTE.red, width: 3.2 },
|
];
|
||||||
];
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 同一期间的多条线将标签放到离最近相邻值更远的一侧,确保每个数值都可见。
|
||||||
|
*/
|
||||||
|
function labeledData(rows, key) {
|
||||||
|
return rows.map((row) => {
|
||||||
|
const value = row[key];
|
||||||
|
const ranked = SERIES.map((series) => row[series.key]).sort((a, b) => b - a);
|
||||||
|
const rank = ranked.indexOf(value);
|
||||||
|
const gapUp = rank > 0 ? ranked[rank - 1] - value : Infinity;
|
||||||
|
const gapDown = rank < ranked.length - 1 ? value - ranked[rank + 1] : Infinity;
|
||||||
|
|
||||||
|
return {
|
||||||
|
value,
|
||||||
|
label: { position: gapUp >= gapDown ? 'top' : 'bottom', distance: 9 },
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function workingCapitalDaysChart(rows, { width, height }) {
|
||||||
return {
|
return {
|
||||||
backgroundColor: '#fff',
|
backgroundColor: '#fff',
|
||||||
title: {
|
title: {
|
||||||
...titleStyle,
|
...titleStyle,
|
||||||
text: '泡泡玛特营运资本效率',
|
text: '泡泡玛特营运资本效率',
|
||||||
subtext: '现金转换周期 = 存货周转天数 + 应收周转天数 − 应付周转天数',
|
subtext: '现金转换周期 = 存货周转天数 + 应收周转天数 − 应付周转天数(中报按181天、年报按365天)',
|
||||||
},
|
},
|
||||||
legend: { ...legendStyle, top: LAYOUT.legendTop, data: series.map((item) => item.name) },
|
legend: { ...legendStyle, top: LAYOUT.legendTop, data: SERIES.map((item) => item.name) },
|
||||||
grid: { left: 78, right: 42, top: LAYOUT.gridTop, bottom: 90 },
|
grid: { left: 78, right: 42, top: LAYOUT.gridTop, bottom: 90 },
|
||||||
tooltip: {
|
tooltip: {
|
||||||
trigger: 'axis',
|
trigger: 'axis',
|
||||||
@@ -37,22 +55,30 @@ export function workingCapitalDaysChart(rows, { width, height }) {
|
|||||||
nameTextStyle: { color: PALETTE.grey, fontSize: 13, fontFamily: FONT },
|
nameTextStyle: { color: PALETTE.grey, fontSize: 13, fontFamily: FONT },
|
||||||
min: 0,
|
min: 0,
|
||||||
},
|
},
|
||||||
series: series.map((item) => ({
|
series: SERIES.map((item) => ({
|
||||||
...lineSeries({
|
...lineSeries({
|
||||||
name: item.name,
|
name: item.name,
|
||||||
data: col(rows, item.key),
|
data: labeledData(rows, item.key),
|
||||||
color: item.color,
|
color: item.color,
|
||||||
formatter: (p) => p.value.toFixed(0),
|
formatter: (p) => p.value.toFixed(0),
|
||||||
width: item.width,
|
width: item.width,
|
||||||
}),
|
}),
|
||||||
label: item.key === 'cash_conversion_cycle'
|
label: {
|
||||||
? { show: true, color: item.color, fontFamily: FONT, fontSize: 11, formatter: (p) => `${p.value.toFixed(0)}天` }
|
show: true,
|
||||||
: { show: false },
|
color: item.color,
|
||||||
|
fontFamily: FONT,
|
||||||
|
fontSize: 11,
|
||||||
|
fontWeight: 500,
|
||||||
|
formatter: (p) => `${p.value.toFixed(0)}天`,
|
||||||
|
textBorderColor: '#fff',
|
||||||
|
textBorderWidth: 3,
|
||||||
|
},
|
||||||
|
labelLayout: { hideOverlap: true },
|
||||||
z: item.key === 'cash_conversion_cycle' ? 10 : 1,
|
z: item.key === 'cash_conversion_cycle' ? 10 : 1,
|
||||||
})),
|
})),
|
||||||
graphic: [
|
graphic: [
|
||||||
...watermarks(width, height),
|
...watermarks(width, height),
|
||||||
footnote('数据来源:泡泡玛特财务指标(合并报表)| @思考的Joey'),
|
footnote('数据来源:泡泡玛特财务指标(合并报表);原始180/360天口径已换算为181/365天 | @思考的Joey'),
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
+35
-5
@@ -11,6 +11,7 @@ import {
|
|||||||
|
|
||||||
const instances = new Map();
|
const instances = new Map();
|
||||||
const activeViews = new Map();
|
const activeViews = new Map();
|
||||||
|
const chartRenderVersions = new Map();
|
||||||
let manifest;
|
let manifest;
|
||||||
let shell;
|
let shell;
|
||||||
let state;
|
let state;
|
||||||
@@ -106,25 +107,47 @@ function chartSource(chart) {
|
|||||||
return chart.views[view];
|
return chart.views[view];
|
||||||
}
|
}
|
||||||
|
|
||||||
async function renderChart(chart, version) {
|
async function renderChart(chart, version, chartVersion) {
|
||||||
const el = document.getElementById(`chart-${chart.instanceId}`);
|
const el = document.getElementById(`chart-${chart.instanceId}`);
|
||||||
if (!el) return;
|
if (!el) return;
|
||||||
const source = chartSource(chart);
|
const source = chartSource(chart);
|
||||||
|
const view = chart.views
|
||||||
|
? activeViews.get(chart.viewKey) || chart.defaultView
|
||||||
|
: undefined;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const rows = await loadCSV(`${source}?t=${cacheToken}`);
|
const rows = await loadCSV(`${source}?t=${cacheToken}`);
|
||||||
if (version !== renderVersion || !document.body.contains(el)) return;
|
if (version !== renderVersion || chartVersion !== chartRenderVersions.get(chart.instanceId) || !document.body.contains(el)) return;
|
||||||
|
|
||||||
const instance = echarts.init(el, null, { renderer: 'canvas' });
|
const instance = echarts.init(el, null, { renderer: 'canvas' });
|
||||||
instances.set(chart.instanceId, instance);
|
instances.set(chart.instanceId, instance);
|
||||||
const { width, height } = el.getBoundingClientRect();
|
const { width, height } = el.getBoundingClientRect();
|
||||||
instance.setOption(getChartBuilder(chart.key)(rows, { width, height }), true);
|
instance.setOption(getChartBuilder(chart.key)(rows, { width, height, view }), true);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (version === renderVersion) showChartError(chart.instanceId, error.message);
|
if (version === renderVersion) showChartError(chart.instanceId, error.message);
|
||||||
console.error(`渲染图表失败:${chart.id}`, error);
|
console.error(`渲染图表失败:${chart.id}`, error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderSingleChart(chart) {
|
||||||
|
const previous = instances.get(chart.instanceId);
|
||||||
|
previous?.dispose();
|
||||||
|
instances.delete(chart.instanceId);
|
||||||
|
const chartVersion = (chartRenderVersions.get(chart.instanceId) || 0) + 1;
|
||||||
|
chartRenderVersions.set(chart.instanceId, chartVersion);
|
||||||
|
renderChart(chart, renderVersion, chartVersion);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateViewControls(chart, view) {
|
||||||
|
const card = document.getElementById(`card-${chart.instanceId}`);
|
||||||
|
if (!card) return;
|
||||||
|
card.querySelectorAll('.view-button').forEach((button) => {
|
||||||
|
button.dataset.active = String(button.dataset.view === view);
|
||||||
|
});
|
||||||
|
const source = card.querySelector('.src');
|
||||||
|
if (source) source.textContent = chartSource(chart);
|
||||||
|
}
|
||||||
|
|
||||||
function exportPNG(chartId, filename) {
|
function exportPNG(chartId, filename) {
|
||||||
const instance = instances.get(chartId);
|
const instance = instances.get(chartId);
|
||||||
if (!instance) return;
|
if (!instance) return;
|
||||||
@@ -167,12 +190,19 @@ function renderCurrent() {
|
|||||||
renderCards(shell.cards, charts, activeViews, {
|
renderCards(shell.cards, charts, activeViews, {
|
||||||
onExport: exportPNG,
|
onExport: exportPNG,
|
||||||
onSelectView: (chart, view) => {
|
onSelectView: (chart, view) => {
|
||||||
|
if (activeViews.get(chart.viewKey) === view) return;
|
||||||
activeViews.set(chart.viewKey, view);
|
activeViews.set(chart.viewKey, view);
|
||||||
updateHash({ companyId: company.id, tabId: tab.id, chartId: chart.id, view });
|
state = normalizeState({ companyId: company.id, tabId: tab.id, chartId: chart.id, view });
|
||||||
|
history.pushState(null, '', serializeHash(state));
|
||||||
|
updateViewControls(chart, view);
|
||||||
|
renderSingleChart(chart);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
renderAnchors(shell.chartNav, charts, selectAnchor);
|
renderAnchors(shell.chartNav, charts, selectAnchor);
|
||||||
Promise.all(charts.map((chart) => renderChart(chart, version)));
|
charts.forEach((chart) => {
|
||||||
|
chartRenderVersions.set(chart.instanceId, (chartRenderVersions.get(chart.instanceId) || 0) + 1);
|
||||||
|
});
|
||||||
|
Promise.all(charts.map((chart) => renderChart(chart, version, chartRenderVersions.get(chart.instanceId))));
|
||||||
}
|
}
|
||||||
|
|
||||||
async function start() {
|
async function start() {
|
||||||
|
|||||||
+60
-17
@@ -28,24 +28,34 @@ export const PALETTE = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 试行语义色:柱线组合图中,柱采用低饱和辅助色,折线保留单一强调色。
|
* 正式色板:轻快消费。
|
||||||
* 待页面视觉确认后再固化为新增图表的正式规范。
|
* 柱线组合中,柱用不同色相承载分类,折线保留高辨识度的单一强调色。
|
||||||
*/
|
*/
|
||||||
export const FINANCIAL_COLORS = {
|
export const FINANCIAL_COLORS = {
|
||||||
operatingCashFlow: '#6E9A83',
|
operatingCashFlow: '#36A28F',
|
||||||
investingCashFlow: '#7897B2',
|
investingCashFlow: '#4D83D1',
|
||||||
financingCashFlow: '#B18A66',
|
financingCashFlow: '#EEA64F',
|
||||||
netCashIncrease: '#A8465C',
|
netCashIncrease: '#D95567',
|
||||||
netProfit: '#C9885C',
|
netProfit: '#F08B5A',
|
||||||
cashConversion: '#6B4A6C',
|
cashConversion: '#8256B2',
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 瀑布图语义色:基准值、正向贡献、负向贡献。
|
||||||
|
* 复用轻快消费财务色板,避免利润率拆解图另起一套颜色。
|
||||||
|
*/
|
||||||
|
export const BRIDGE_COLORS = {
|
||||||
|
total: FINANCIAL_COLORS.investingCashFlow,
|
||||||
|
pos: FINANCIAL_COLORS.operatingCashFlow,
|
||||||
|
neg: FINANCIAL_COLORS.netCashIncrease,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const CHANNEL_COLORS = {
|
export const CHANNEL_COLORS = {
|
||||||
retail: '#597A9B',
|
retail: '#3F7FBE',
|
||||||
online: '#C87368',
|
online: '#E36D63',
|
||||||
robotStore: '#98A77A',
|
robotStore: '#71AD81',
|
||||||
wholesale: '#BB9A5C',
|
wholesale: '#E4A349',
|
||||||
revenueGrowth: '#775D8B',
|
revenueGrowth: '#8256B2',
|
||||||
};
|
};
|
||||||
|
|
||||||
/** 坐标轴通用样式:只留浅色横向网格,去掉轴线的存在感 */
|
/** 坐标轴通用样式:只留浅色横向网格,去掉轴线的存在感 */
|
||||||
@@ -98,6 +108,32 @@ export const periodAxis = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 独立期间轴:FY 为全年,H1/H2 为单独半年。
|
||||||
|
* 用于已拆分累计值的多时间维度图,避免把 H2 错写为全年。
|
||||||
|
*/
|
||||||
|
export function shortStandalonePeriod(label) {
|
||||||
|
return String(label)
|
||||||
|
.replace(/^20(\d{2})FY$/, '$1全年')
|
||||||
|
.replace(/^20(\d{2})/, '$1');
|
||||||
|
}
|
||||||
|
|
||||||
|
export const standalonePeriodAxis = {
|
||||||
|
...categoryAxis,
|
||||||
|
axisLabel: {
|
||||||
|
...categoryAxis.axisLabel,
|
||||||
|
formatter: shortStandalonePeriod,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export function financialViewLabel(view) {
|
||||||
|
return ({ all: '全部期间', annual: '年度', halfyear: '半年', quarter: '季度' })[view] || '全部期间';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function financialViewDisclosure(view) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
export const titleStyle = {
|
export const titleStyle = {
|
||||||
left: 'center',
|
left: 'center',
|
||||||
top: 18,
|
top: 18,
|
||||||
@@ -140,7 +176,7 @@ export const legendStyle = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
/** 折线标注:带白底描边,避免压在网格线上看不清 */
|
/** 折线标注:白底描边,避免标签被折线自身或柱体穿过时看不清 */
|
||||||
export function labelStyle(color, position = 'top') {
|
export function labelStyle(color, position = 'top') {
|
||||||
return {
|
return {
|
||||||
show: true,
|
show: true,
|
||||||
@@ -150,6 +186,8 @@ export function labelStyle(color, position = 'top') {
|
|||||||
fontFamily: FONT,
|
fontFamily: FONT,
|
||||||
fontWeight: 500,
|
fontWeight: 500,
|
||||||
distance: 8,
|
distance: 8,
|
||||||
|
textBorderColor: '#fff',
|
||||||
|
textBorderWidth: 3,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -201,9 +239,12 @@ export function lineSeries({ name, data, color, labelPosition = 'top', formatter
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 柱线组合图的折线标签:正值放在点上方、负值放在点下方;
|
* 柱线组合图的折线标签:正值放在点上方、负值放在点下方;
|
||||||
* 若与同期间柱值接近,则加大距离,避免文字压在柱体端点附近。
|
* 若与同期间柱值接近,则加大距离;也可指定将标签放到柱体另一侧,
|
||||||
|
* 避免文字压在柱体端点附近。
|
||||||
*/
|
*/
|
||||||
export function lineDataWithSafeLabels(rows, { lineKey, barKeys, distance = 8, closeDistance = 14 }) {
|
export function lineDataWithSafeLabels(rows, {
|
||||||
|
lineKey, barKeys, distance = 8, closeDistance = 14, closePosition,
|
||||||
|
}) {
|
||||||
const values = rows.map((row) => row[lineKey]).filter(Number.isFinite);
|
const values = rows.map((row) => row[lineKey]).filter(Number.isFinite);
|
||||||
const range = Math.max(...values) - Math.min(...values) || 1;
|
const range = Math.max(...values) - Math.min(...values) || 1;
|
||||||
const threshold = range * 0.06;
|
const threshold = range * 0.06;
|
||||||
@@ -217,7 +258,9 @@ export function lineDataWithSafeLabels(rows, { lineKey, barKeys, distance = 8, c
|
|||||||
return {
|
return {
|
||||||
value,
|
value,
|
||||||
label: {
|
label: {
|
||||||
position: value < 0 ? 'bottom' : 'top',
|
position: nearestBarGap <= threshold && closePosition
|
||||||
|
? closePosition
|
||||||
|
: (value < 0 ? 'bottom' : 'top'),
|
||||||
distance: nearestBarGap <= threshold ? closeDistance : distance,
|
distance: nearestBarGap <= threshold ? closeDistance : distance,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -79,13 +79,15 @@ export function renderCards(container, charts, activeViews, { onExport, onSelect
|
|||||||
|
|
||||||
if (viewNames.length > 1) {
|
if (viewNames.length > 1) {
|
||||||
const switcher = element('div', 'view-switcher');
|
const switcher = element('div', 'view-switcher');
|
||||||
const labels = { annual: '年度', halfyear: '半年', quarter: '季度' };
|
const labels = { all: '全部', annual: '年度', halfyear: '半年', quarter: '季度' };
|
||||||
viewNames.forEach((view) => {
|
viewNames.forEach((view) => {
|
||||||
switcher.append(button(labels[view] || view, {
|
const viewButton = button(labels[view] || view, {
|
||||||
active: view === activeView,
|
active: view === activeView,
|
||||||
onClick: () => onSelectView(chart, view),
|
onClick: () => onSelectView(chart, view),
|
||||||
className: 'view-button',
|
className: 'view-button',
|
||||||
}));
|
});
|
||||||
|
viewButton.dataset.view = view;
|
||||||
|
switcher.append(viewButton);
|
||||||
});
|
});
|
||||||
bar.append(switcher);
|
bar.append(switcher);
|
||||||
}
|
}
|
||||||
|
|||||||
+25
-17
@@ -4,36 +4,44 @@
|
|||||||
"id": "ppmt",
|
"id": "ppmt",
|
||||||
"name": "泡泡玛特",
|
"name": "泡泡玛特",
|
||||||
"chartCatalog": {
|
"chartCatalog": {
|
||||||
"ppmt-profit": { "key": "ppmt-profit", "title": "盈利能力", "csv": "data/ppmt/ppmt_profit.csv", "height": 680, "filename": "泡泡玛特盈利能力" },
|
"ppmt-profit": { "key": "ppmt-profit", "title": "盈利能力", "defaultView": "all", "views": { "all": "data/ppmt/ppmt_profit.csv", "annual": "data/ppmt/ppmt_profit_annual.csv", "halfyear": "data/ppmt/ppmt_profit_halfyear.csv" }, "height": 680, "filename": "泡泡玛特盈利能力" },
|
||||||
"ppmt-sankey": { "key": "ppmt-sankey", "title": "利润流向", "csv": "data/ppmt/ppmt_sankey_2026h1.csv", "height": 800, "filename": "泡泡玛特利润流向2026H1" },
|
"ppmt-sankey": { "key": "ppmt-sankey", "title": "利润流向", "csv": "data/ppmt/ppmt_sankey_2026h1.csv", "height": 800, "filename": "泡泡玛特利润流向2026H1" },
|
||||||
"ppmt-growth": { "key": "ppmt-growth", "title": "营收到利润的增速对比", "csv": "data/ppmt/ppmt_growth.csv", "height": 700, "filename": "泡泡玛特营收到利润增速对比" },
|
"ppmt-growth": { "key": "ppmt-growth", "title": "营收到利润的增速对比", "defaultView": "all", "views": { "all": "data/ppmt/ppmt_growth.csv", "annual": "data/ppmt/ppmt_growth_annual.csv", "halfyear": "data/ppmt/ppmt_growth_halfyear.csv" }, "height": 700, "filename": "泡泡玛特营收到利润增速对比" },
|
||||||
"ppmt-core-profit": { "key": "ppmt-core-profit", "title": "核心利润及同比增速", "csv": "data/ppmt/ppmt_core_profit.csv", "height": 720, "filename": "泡泡玛特核心利润及同比增速" },
|
"ppmt-core-profit": { "key": "ppmt-core-profit", "title": "核心利润及同比增速", "defaultView": "all", "views": { "all": "data/ppmt/ppmt_core_profit.csv", "annual": "data/ppmt/ppmt_core_profit_annual.csv", "halfyear": "data/ppmt/ppmt_core_profit_halfyear.csv" }, "height": 720, "filename": "泡泡玛特核心利润及同比增速" },
|
||||||
"ppmt-core-margin": { "key": "ppmt-core-margin", "title": "核心利润率", "csv": "data/ppmt/ppmt_core_margin.csv", "height": 680, "filename": "泡泡玛特核心利润率" },
|
"ppmt-core-margin": { "key": "ppmt-core-margin", "title": "核心利润率", "defaultView": "all", "views": { "all": "data/ppmt/ppmt_core_margin.csv", "annual": "data/ppmt/ppmt_core_margin_annual.csv", "halfyear": "data/ppmt/ppmt_core_margin_halfyear.csv" }, "height": 680, "filename": "泡泡玛特核心利润率" },
|
||||||
"ppmt-bridge": { "key": "ppmt-bridge", "title": "净利率同比拆解", "csv": "data/ppmt/ppmt_margin_bridge.csv", "height": 700, "filename": "泡泡玛特净利率同比拆解" },
|
"ppmt-bridge": { "key": "ppmt-bridge", "title": "净利率同比拆解", "csv": "data/ppmt/ppmt_margin_bridge.csv", "height": 700, "filename": "泡泡玛特净利率同比拆解" },
|
||||||
"ppmt-margins": { "key": "ppmt-margins", "title": "利润率与费用率拆解", "csv": "data/ppmt/ppmt_margins.csv", "height": 680, "filename": "泡泡玛特利润率与费用率拆解" },
|
"ppmt-margins": { "key": "ppmt-margins", "title": "利润率与费用率拆解", "defaultView": "all", "views": { "all": "data/ppmt/ppmt_margins.csv", "annual": "data/ppmt/ppmt_margins_annual.csv", "halfyear": "data/ppmt/ppmt_margins_halfyear.csv" }, "height": 680, "filename": "泡泡玛特利润率与费用率拆解" },
|
||||||
"ppmt-assets": { "key": "ppmt-assets", "title": "资产结构演变", "csv": "data/ppmt/ppmt_assets.csv", "height": 680, "filename": "泡泡玛特资产结构" },
|
"ppmt-assets": { "key": "ppmt-assets", "title": "资产结构演变", "defaultView": "all", "views": { "all": "data/ppmt/ppmt_assets.csv", "annual": "data/ppmt/ppmt_assets_annual.csv", "halfyear": "data/ppmt/ppmt_assets_halfyear.csv" }, "height": 680, "filename": "泡泡玛特资产结构" },
|
||||||
"ppmt-ip": { "key": "ppmt-ip", "title": "主要 IP 收入趋势", "csv": "data/ppmt/ppmt_ip.csv", "height": 720, "filename": "泡泡玛特IP收入趋势" },
|
"ppmt-inventory": { "key": "ppmt-inventory", "title": "存货余额及同比", "defaultView": "all", "views": { "all": "data/ppmt/ppmt_inventory.csv", "annual": "data/ppmt/ppmt_inventory_annual.csv", "halfyear": "data/ppmt/ppmt_inventory_halfyear.csv" }, "height": 680, "filename": "泡泡玛特存货余额及同比" },
|
||||||
|
"ppmt-ip": { "key": "ppmt-ip", "title": "主要 IP 收入趋势", "defaultView": "all", "views": { "all": "data/ppmt/ppmt_ip.csv", "annual": "data/ppmt/ppmt_ip_annual.csv", "halfyear": "data/ppmt/ppmt_ip_halfyear.csv" }, "height": 720, "filename": "泡泡玛特IP收入趋势" },
|
||||||
"ppmt-ip-share": { "key": "ppmt-ip-share", "title": "主要 IP 收入占比", "csv": "data/ppmt/ppmt_ip_share_2026h1.csv", "height": 720, "filename": "泡泡玛特IP收入占比2026H1" },
|
"ppmt-ip-share": { "key": "ppmt-ip-share", "title": "主要 IP 收入占比", "csv": "data/ppmt/ppmt_ip_share_2026h1.csv", "height": 720, "filename": "泡泡玛特IP收入占比2026H1" },
|
||||||
"ppmt-ip-share-history": { "key": "ppmt-ip-share-history", "title": "各 IP 半年收入占比", "csv": "data/ppmt/ppmt_ip_share_history.csv", "height": 700, "filename": "泡泡玛特各IP半年收入占比" },
|
"ppmt-ip-share-history": { "key": "ppmt-ip-share-history", "title": "各 IP 半年收入占比", "defaultView": "all", "views": { "all": "data/ppmt/ppmt_ip_share_history.csv", "annual": "data/ppmt/ppmt_ip_share_history_annual.csv", "halfyear": "data/ppmt/ppmt_ip_share_history_halfyear.csv" }, "height": 700, "filename": "泡泡玛特各IP半年收入占比" },
|
||||||
"ppmt-ip-ex-monsters": { "key": "ppmt-ip-ex-monsters", "title": "除 THE MONSTERS 外的 IP 收入及同比", "csv": "data/ppmt/ppmt_ip_ex_monsters.csv", "height": 700, "filename": "泡泡玛特除MONSTERS外IP收入及同比" },
|
"ppmt-ip-ex-monsters": { "key": "ppmt-ip-ex-monsters", "title": "除 THE MONSTERS 外的 IP 收入及同比", "defaultView": "all", "views": { "all": "data/ppmt/ppmt_ip_ex_monsters.csv", "annual": "data/ppmt/ppmt_ip_ex_monsters_annual.csv", "halfyear": "data/ppmt/ppmt_ip_ex_monsters_halfyear.csv" }, "height": 700, "filename": "泡泡玛特除MONSTERS外IP收入及同比" },
|
||||||
"ppmt-category": { "key": "ppmt-category", "title": "品类收入结构", "csv": "data/ppmt/ppmt_category.csv", "height": 680, "filename": "泡泡玛特品类结构" },
|
"ppmt-category": { "key": "ppmt-category", "title": "品类收入结构", "defaultView": "all", "views": { "all": "data/ppmt/ppmt_category.csv", "annual": "data/ppmt/ppmt_category_annual.csv", "halfyear": "data/ppmt/ppmt_category_halfyear.csv" }, "height": 680, "filename": "泡泡玛特品类结构" },
|
||||||
"ppmt-region-channel": { "key": "ppmt-region-channel", "title": "区域渠道分布", "csv": "data/ppmt/ppmt_region_channel_2026h1.csv", "height": 680, "filename": "泡泡玛特区域渠道分布2026H1" },
|
"ppmt-region-channel": { "key": "ppmt-region-channel", "title": "区域渠道分布", "csv": "data/ppmt/ppmt_region_channel_2026h1.csv", "height": 680, "filename": "泡泡玛特区域渠道分布2026H1" },
|
||||||
"ppmt-region-yoy": { "key": "ppmt-region-yoy", "title": "区域渠道同比增速", "csv": "data/ppmt/ppmt_region_channel_yoy.csv", "height": 620, "filename": "泡泡玛特区域渠道同比增速" },
|
"ppmt-region-yoy": { "key": "ppmt-region-yoy", "title": "区域渠道同比增速", "csv": "data/ppmt/ppmt_region_channel_yoy.csv", "height": 620, "filename": "泡泡玛特区域渠道同比增速" },
|
||||||
"ppmt-region-yoy-timeseries": { "key": "ppmt-region-yoy-timeseries", "title": "区域渠道同比增速趋势", "csv": "data/ppmt/ppmt_region_channel_yoy_timeseries.csv", "height": 760, "filename": "泡泡玛特区域渠道同比增速趋势" },
|
"ppmt-region-yoy-timeseries": { "key": "ppmt-region-yoy-timeseries", "title": "区域渠道同比增速趋势", "csv": "data/ppmt/ppmt_region_channel_yoy_timeseries.csv", "height": 760, "filename": "泡泡玛特区域渠道同比增速趋势" },
|
||||||
"ppmt-cash-conversion": { "key": "ppmt-cash-conversion", "title": "经营现金流与净利润(年度)", "csv": "data/ppmt/ppmt_cash_conversion.csv", "height": 700, "filename": "泡泡玛特经营现金流与净利润" },
|
"ppmt-cash-conversion": { "key": "ppmt-cash-conversion", "title": "经营现金流与净利润", "defaultView": "all", "views": { "all": "data/ppmt/ppmt_cash_conversion.csv", "annual": "data/ppmt/ppmt_cash_conversion_annual.csv", "halfyear": "data/ppmt/ppmt_cash_conversion_halfyear.csv" }, "height": 700, "filename": "泡泡玛特经营现金流与净利润" },
|
||||||
"ppmt-liquidity-working-capital": { "key": "ppmt-liquidity-working-capital", "title": "现金、负债与净营运资本", "csv": "data/ppmt/ppmt_liquidity_working_capital.csv", "height": 720, "filename": "泡泡玛特现金负债与净营运资本" },
|
"ppmt-liquidity-working-capital": { "key": "ppmt-liquidity-working-capital", "title": "现金、负债与净营运资本", "defaultView": "all", "views": { "all": "data/ppmt/ppmt_liquidity_working_capital.csv", "annual": "data/ppmt/ppmt_liquidity_working_capital_annual.csv", "halfyear": "data/ppmt/ppmt_liquidity_working_capital_halfyear.csv" }, "height": 720, "filename": "泡泡玛特现金负债与净营运资本" },
|
||||||
"ppmt-cashflow-composition": { "key": "ppmt-cashflow-composition", "title": "三大现金流与现金净增加(年度)", "csv": "data/ppmt/ppmt_cashflow_composition.csv", "height": 700, "filename": "泡泡玛特三大现金流与现金净增加" },
|
"ppmt-cashflow-composition": { "key": "ppmt-cashflow-composition", "title": "三大现金流与现金净增加", "defaultView": "all", "views": { "all": "data/ppmt/ppmt_cashflow_composition.csv", "annual": "data/ppmt/ppmt_cashflow_composition_annual.csv", "halfyear": "data/ppmt/ppmt_cashflow_composition_halfyear.csv" }, "height": 700, "filename": "泡泡玛特三大现金流与现金净增加" },
|
||||||
"ppmt-working-capital-days": { "key": "ppmt-working-capital-days", "title": "营运资本效率", "csv": "data/ppmt/ppmt_working_capital_days.csv", "height": 700, "filename": "泡泡玛特营运资本效率" },
|
"ppmt-working-capital-days": { "key": "ppmt-working-capital-days", "title": "营运资本效率", "csv": "data/ppmt/ppmt_working_capital_days.csv", "height": 700, "filename": "泡泡玛特营运资本效率" },
|
||||||
"ppmt-expense-structure": { "key": "ppmt-expense-structure", "title": "费用结构与费用率(年度)", "csv": "data/ppmt/ppmt_expense_structure.csv", "height": 700, "filename": "泡泡玛特费用结构与费用率" },
|
"ppmt-expense-structure": { "key": "ppmt-expense-structure", "title": "费用结构与费用率", "defaultView": "all", "views": { "all": "data/ppmt/ppmt_expense_structure.csv", "annual": "data/ppmt/ppmt_expense_structure_annual.csv", "halfyear": "data/ppmt/ppmt_expense_structure_halfyear.csv" }, "height": 700, "filename": "泡泡玛特费用结构与费用率" },
|
||||||
"ppmt-channel-revenue": { "key": "ppmt-channel-revenue", "title": "渠道收入结构与增速", "csv": "data/ppmt/ppmt_channel_revenue.csv", "height": 720, "filename": "泡泡玛特渠道收入结构与增速" }
|
"ppmt-channel-revenue": { "key": "ppmt-channel-revenue", "title": "渠道收入结构与增速", "defaultView": "all", "views": { "all": "data/ppmt/ppmt_channel_revenue.csv", "annual": "data/ppmt/ppmt_channel_revenue_annual.csv", "halfyear": "data/ppmt/ppmt_channel_revenue_halfyear.csv" }, "height": 720, "filename": "泡泡玛特渠道收入结构与增速" },
|
||||||
|
"ppmt-stores-retail": { "key": "ppmt-stores-retail", "title": "分地区零售门店数量与同比增速", "defaultView": "all", "views": { "all": "data/ppmt/ppmt_stores_retail.csv", "annual": "data/ppmt/ppmt_stores_retail_annual.csv", "halfyear": "data/ppmt/ppmt_stores_retail_halfyear.csv" }, "height": 680, "filename": "泡泡玛特分地区零售门店数量与同比增速" },
|
||||||
|
"ppmt-stores-retail-share": { "key": "ppmt-stores-retail-share", "title": "分地区零售门店数量占比", "defaultView": "all", "views": { "all": "data/ppmt/ppmt_stores_retail_share.csv", "annual": "data/ppmt/ppmt_stores_retail_share_annual.csv", "halfyear": "data/ppmt/ppmt_stores_retail_share_halfyear.csv" }, "height": 680, "filename": "泡泡玛特分地区零售门店数量占比" },
|
||||||
|
"ppmt-retail-store-revenue": { "key": "ppmt-retail-store-revenue", "title": "各地区零售店单店收入", "defaultView": "all", "views": { "all": "data/ppmt/ppmt_retail_store_revenue.csv", "annual": "data/ppmt/ppmt_retail_store_revenue_annual.csv", "halfyear": "data/ppmt/ppmt_retail_store_revenue_halfyear.csv" }, "height": 680, "filename": "泡泡玛特各地区零售店单店收入" },
|
||||||
|
"ppmt-stores-robot": { "key": "ppmt-stores-robot", "title": "分地区机器人商店数量与同比增速", "defaultView": "all", "views": { "all": "data/ppmt/ppmt_stores_robot.csv", "annual": "data/ppmt/ppmt_stores_robot_annual.csv", "halfyear": "data/ppmt/ppmt_stores_robot_halfyear.csv" }, "height": 680, "filename": "泡泡玛特分地区机器人商店数量与同比增速" },
|
||||||
|
"ppmt-robot-store-revenue": { "key": "ppmt-robot-store-revenue", "title": "机器人商店单店收入", "defaultView": "all", "views": { "all": "data/ppmt/ppmt_robot_store_revenue.csv", "annual": "data/ppmt/ppmt_robot_store_revenue_annual.csv", "halfyear": "data/ppmt/ppmt_robot_store_revenue_halfyear.csv" }, "height": 680, "filename": "泡泡玛特机器人商店单店收入" },
|
||||||
|
"ppmt-region-share": { "key": "ppmt-region-share", "title": "分地区营收占比", "defaultView": "all", "views": { "all": "data/ppmt/ppmt_region_share.csv", "annual": "data/ppmt/ppmt_region_share_annual.csv", "halfyear": "data/ppmt/ppmt_region_share_halfyear.csv" }, "height": 680, "filename": "泡泡玛特分地区营收占比" }
|
||||||
},
|
},
|
||||||
"tabs": [
|
"tabs": [
|
||||||
{ "id": "overview", "name": "概览", "chartRefs": ["ppmt-growth", "ppmt-profit", "ppmt-ip-share", "ppmt-region-channel"] },
|
{ "id": "overview", "name": "概览", "chartRefs": ["ppmt-growth", "ppmt-profit", "ppmt-ip-share", "ppmt-region-channel"] },
|
||||||
{ "id": "profitability", "name": "盈利能力", "chartRefs": ["ppmt-growth", "ppmt-core-profit", "ppmt-core-margin", "ppmt-bridge", "ppmt-sankey", "ppmt-cash-conversion", "ppmt-cashflow-composition"] },
|
{ "id": "profitability", "name": "盈利能力", "chartRefs": ["ppmt-growth", "ppmt-core-profit", "ppmt-core-margin", "ppmt-bridge", "ppmt-sankey", "ppmt-cash-conversion", "ppmt-cashflow-composition"] },
|
||||||
{ "id": "expense", "name": "费用率", "chartRefs": ["ppmt-margins", "ppmt-expense-structure"] },
|
{ "id": "expense", "name": "费用率", "chartRefs": ["ppmt-margins", "ppmt-expense-structure"] },
|
||||||
{ "id": "balance-sheet", "name": "资产负债", "chartRefs": ["ppmt-assets", "ppmt-liquidity-working-capital", "ppmt-working-capital-days"] },
|
{ "id": "balance-sheet", "name": "资产负债", "chartRefs": ["ppmt-assets", "ppmt-inventory", "ppmt-liquidity-working-capital", "ppmt-working-capital-days"] },
|
||||||
{ "id": "ip-category", "name": "IP 与品类", "chartRefs": ["ppmt-ip", "ppmt-ip-share", "ppmt-ip-share-history", "ppmt-ip-ex-monsters", "ppmt-category"] },
|
{ "id": "ip-category", "name": "IP 与品类", "chartRefs": ["ppmt-ip", "ppmt-ip-share", "ppmt-ip-share-history", "ppmt-ip-ex-monsters", "ppmt-category"] },
|
||||||
{ "id": "region-channel", "name": "区域渠道", "chartRefs": ["ppmt-region-channel", "ppmt-region-yoy", "ppmt-region-yoy-timeseries", "ppmt-channel-revenue"] }
|
{ "id": "region-channel", "name": "区域渠道", "chartRefs": ["ppmt-region-channel", "ppmt-region-yoy", "ppmt-region-yoy-timeseries", "ppmt-channel-revenue", "ppmt-stores-retail", "ppmt-stores-retail-share", "ppmt-stores-robot", "ppmt-region-share"] },
|
||||||
|
{ "id": "store-revenue", "name": "单店收入", "chartRefs": ["ppmt-retail-store-revenue", "ppmt-robot-store-revenue"] }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user