feat: 泡泡玛特增加年度、半年数据区分
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { col } from '../csv.js';
|
||||
import {
|
||||
PALETTE, FONT, axisCommon, periodAxis, titleStyle, legendStyle,
|
||||
PALETTE, FONT, axisCommon, standalonePeriodAxis, titleStyle, legendStyle,
|
||||
financialViewLabel,
|
||||
lineSeries, footnote, watermarks, LAYOUT,
|
||||
} 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');
|
||||
|
||||
return {
|
||||
backgroundColor: '#fff',
|
||||
title: {
|
||||
...titleStyle,
|
||||
text: '泡泡玛特资产结构演变(2020H1-2026H1)',
|
||||
subtext: '单位:亿元',
|
||||
text: `泡泡玛特资产结构演变(${financialViewLabel(view)})`,
|
||||
subtext: '单位:亿元;资产负债表为期末余额,H2 为年末时点而非全年累计',
|
||||
},
|
||||
legend: {
|
||||
...legendStyle,
|
||||
@@ -29,7 +30,7 @@ export function assetsChart(rows, { width, height }) {
|
||||
axisPointer: { type: 'shadow' },
|
||||
textStyle: { fontFamily: FONT },
|
||||
},
|
||||
xAxis: { ...periodAxis, data: periods },
|
||||
xAxis: { ...standalonePeriodAxis, data: periods },
|
||||
yAxis: [
|
||||
{
|
||||
...axisCommon,
|
||||
|
||||
@@ -1,19 +1,23 @@
|
||||
import { col } from '../csv.js';
|
||||
import {
|
||||
FINANCIAL_COLORS, PALETTE, FONT, axisCommon, periodAxis, titleStyle, legendStyle,
|
||||
FINANCIAL_COLORS, PALETTE, FONT, axisCommon, standalonePeriodAxis, titleStyle, legendStyle,
|
||||
financialViewDisclosure, financialViewLabel,
|
||||
lineSeries, footnote, watermarks, LAYOUT,
|
||||
} from '../theme.js';
|
||||
|
||||
/** 泡泡玛特经营现金流与净利润:观察利润的现金兑现能力。 */
|
||||
export function cashConversionChart(rows, { width, height }) {
|
||||
export function cashConversionChart(rows, { width, height, view = 'all' }) {
|
||||
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 {
|
||||
backgroundColor: '#fff',
|
||||
title: {
|
||||
...titleStyle,
|
||||
text: '泡泡玛特经营现金流与净利润(年度)',
|
||||
subtext: '现金转换率 = 经营活动现金流净额 / 净利润;单位:亿元',
|
||||
text: `泡泡玛特经营现金流与净利润(${financialViewLabel(view)})`,
|
||||
subtext: ['现金转换率 = 经营活动现金流净额 / 净利润;单位:亿元', financialViewDisclosure(view)].filter(Boolean).join('\n'),
|
||||
},
|
||||
legend: {
|
||||
...legendStyle,
|
||||
@@ -27,7 +31,7 @@ export function cashConversionChart(rows, { width, height }) {
|
||||
textStyle: { fontFamily: FONT },
|
||||
valueFormatter: (value) => `${value.toFixed(1)}`,
|
||||
},
|
||||
xAxis: { ...periodAxis, data: periods },
|
||||
xAxis: { ...standalonePeriodAxis, data: periods },
|
||||
yAxis: [
|
||||
{
|
||||
...axisCommon,
|
||||
@@ -48,8 +52,8 @@ export function cashConversionChart(rows, { width, height }) {
|
||||
nameRotate: -90,
|
||||
nameTextStyle: { color: PALETTE.grey, fontSize: 13, fontFamily: FONT },
|
||||
min: 0,
|
||||
max: 200,
|
||||
interval: 50,
|
||||
max: conversionAxisMax,
|
||||
interval: conversionAxisMax / 4,
|
||||
axisLabel: { ...axisCommon.axisLabel, formatter: '{value}%' },
|
||||
splitLine: { show: false },
|
||||
},
|
||||
@@ -59,7 +63,7 @@ export function cashConversionChart(rows, { width, height }) {
|
||||
name: '经营活动现金流净额',
|
||||
type: 'bar',
|
||||
data: col(rows, 'operating_cash_flow'),
|
||||
barWidth: 31.2,
|
||||
barWidth,
|
||||
itemStyle: { color: FINANCIAL_COLORS.operatingCashFlow },
|
||||
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: '净利润',
|
||||
type: 'bar',
|
||||
data: col(rows, 'net_profit'),
|
||||
barWidth: 31.2,
|
||||
barWidth,
|
||||
itemStyle: { color: FINANCIAL_COLORS.netProfit },
|
||||
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: [
|
||||
...watermarks(width, height),
|
||||
footnote('数据来源:泡泡玛特合并现金流量表、利润表(年度口径)| @思考的Joey'),
|
||||
footnote('数据来源:泡泡玛特合并现金流量表、利润表;H2为单独半年 | @思考的Joey'),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import { col } from '../csv.js';
|
||||
import {
|
||||
FINANCIAL_COLORS, PALETTE, FONT, axisCommon, periodAxis, titleStyle, standardLegend,
|
||||
FINANCIAL_COLORS, PALETTE, FONT, axisCommon, standalonePeriodAxis, titleStyle, standardLegend,
|
||||
financialViewDisclosure, financialViewLabel,
|
||||
lineDataWithSafeLabels,
|
||||
lineSeries, footnote, watermarks, LAYOUT,
|
||||
} 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, {
|
||||
lineKey: 'net_cash_increase',
|
||||
barKeys: ['operating_cash_flow', 'investing_cash_flow', 'financing_cash_flow'],
|
||||
@@ -16,8 +18,8 @@ export function cashflowCompositionChart(rows, { width, height }) {
|
||||
backgroundColor: '#fff',
|
||||
title: {
|
||||
...titleStyle,
|
||||
text: '泡泡玛特三大现金流与现金净增加(年度)',
|
||||
subtext: '单位:亿元;正值为流入,负值为流出',
|
||||
text: `泡泡玛特三大现金流与现金净增加(${financialViewLabel(view)})`,
|
||||
subtext: ['单位:亿元;正值为流入,负值为流出', financialViewDisclosure(view)].filter(Boolean).join('\n'),
|
||||
},
|
||||
// 不手工指定 icon,让 ECharts 按系列类型生成:柱为矩形、折线为短线 + 空心圆。
|
||||
legend: standardLegend(['经营活动现金流', '投资活动现金流', '筹资活动现金流', '现金净增加']),
|
||||
@@ -28,7 +30,7 @@ export function cashflowCompositionChart(rows, { width, height }) {
|
||||
textStyle: { fontFamily: FONT },
|
||||
valueFormatter: (value) => `${value.toFixed(1)} 亿元`,
|
||||
},
|
||||
xAxis: { ...periodAxis, data: col(rows, 'period') },
|
||||
xAxis: { ...standalonePeriodAxis, data: col(rows, 'period') },
|
||||
yAxis: {
|
||||
...axisCommon,
|
||||
type: 'value',
|
||||
@@ -38,9 +40,9 @@ export function cashflowCompositionChart(rows, { width, height }) {
|
||||
nameTextStyle: { color: PALETTE.grey, fontSize: 13, fontFamily: FONT },
|
||||
},
|
||||
series: [
|
||||
{ name: '经营活动现金流', type: 'bar', data: col(rows, 'operating_cash_flow'), barWidth: 26, 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, 'financing_cash_flow'), barWidth: 26, itemStyle: { color: FINANCIAL_COLORS.financingCashFlow } },
|
||||
{ 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, itemStyle: { color: FINANCIAL_COLORS.investingCashFlow } },
|
||||
{ name: '筹资活动现金流', type: 'bar', data: col(rows, 'financing_cash_flow'), barWidth, itemStyle: { color: FINANCIAL_COLORS.financingCashFlow } },
|
||||
{
|
||||
...lineSeries({
|
||||
name: '现金净增加',
|
||||
@@ -55,7 +57,7 @@ export function cashflowCompositionChart(rows, { width, height }) {
|
||||
],
|
||||
graphic: [
|
||||
...watermarks(width, height),
|
||||
footnote('数据来源:泡泡玛特合并现金流量表(年度口径)| @思考的Joey'),
|
||||
footnote('数据来源:泡泡玛特合并现金流量表;H2为单独半年 | @思考的Joey'),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { col } from '../csv.js';
|
||||
import {
|
||||
PALETTE, FONT, axisCommon, categoryAxis, titleStyle, legendStyle,
|
||||
PALETTE, FONT, axisCommon, standalonePeriodAxis, titleStyle, legendStyle,
|
||||
financialViewDisclosure, financialViewLabel,
|
||||
footnote, watermarks, LAYOUT,
|
||||
} 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 periods = col(dataRows, 'period');
|
||||
@@ -17,8 +18,8 @@ export function categoryChart(rows, { width, height }) {
|
||||
backgroundColor: '#fff',
|
||||
title: {
|
||||
...titleStyle,
|
||||
text: '泡泡玛特品类收入结构(2024H1-2025H2)',
|
||||
subtext: '单位:亿元',
|
||||
text: `泡泡玛特品类收入结构(${financialViewLabel(view)})`,
|
||||
subtext: ['单位:亿元;H2为下半年单期', financialViewDisclosure(view)].filter(Boolean).join('\n'),
|
||||
},
|
||||
legend: {
|
||||
...legendStyle,
|
||||
@@ -41,14 +42,7 @@ export function categoryChart(rows, { width, height }) {
|
||||
return result;
|
||||
},
|
||||
},
|
||||
xAxis: {
|
||||
...categoryAxis,
|
||||
data: periods,
|
||||
axisLabel: {
|
||||
...categoryAxis.axisLabel,
|
||||
formatter: (value) => String(value).replace(/^20/, ''),
|
||||
},
|
||||
},
|
||||
xAxis: { ...standalonePeriodAxis, data: periods },
|
||||
yAxis: {
|
||||
...axisCommon,
|
||||
type: 'value',
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { col } from '../csv.js';
|
||||
import {
|
||||
CHANNEL_COLORS, PALETTE, FONT, axisCommon, periodAxis, titleStyle, legendStyle,
|
||||
CHANNEL_COLORS, PALETTE, FONT, axisCommon, standalonePeriodAxis, titleStyle, legendStyle,
|
||||
financialViewDisclosure, financialViewLabel,
|
||||
lineSeries, footnote, watermarks, LAYOUT,
|
||||
} from '../theme.js';
|
||||
|
||||
/** 泡泡玛特渠道收入结构与增速。 */
|
||||
export function channelRevenueChart(rows, { width, height }) {
|
||||
export function channelRevenueChart(rows, { width, height, view = 'all' }) {
|
||||
const channels = [
|
||||
{ key: 'retail', name: '零售店', color: CHANNEL_COLORS.retail },
|
||||
{ key: 'online', name: '线上', color: CHANNEL_COLORS.online },
|
||||
@@ -17,8 +18,8 @@ export function channelRevenueChart(rows, { width, height }) {
|
||||
backgroundColor: '#fff',
|
||||
title: {
|
||||
...titleStyle,
|
||||
text: '泡泡玛特渠道收入结构与增速',
|
||||
subtext: '渠道收入为堆叠柱;折线为营业收入同比增速',
|
||||
text: `泡泡玛特渠道收入结构与增速(${financialViewLabel(view)})`,
|
||||
subtext: ['渠道收入为堆叠柱;折线为营业收入同比增速', financialViewDisclosure(view)].filter(Boolean).join('\n'),
|
||||
},
|
||||
legend: {
|
||||
...legendStyle,
|
||||
@@ -32,7 +33,7 @@ export function channelRevenueChart(rows, { width, height }) {
|
||||
textStyle: { fontFamily: FONT },
|
||||
valueFormatter: (value) => value.toFixed(1),
|
||||
},
|
||||
xAxis: { ...periodAxis, data: col(rows, 'period') },
|
||||
xAxis: { ...standalonePeriodAxis, data: col(rows, 'period') },
|
||||
yAxis: [
|
||||
{
|
||||
...axisCommon,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { col } from '../csv.js';
|
||||
import {
|
||||
PALETTE, FONT, axisCommon, periodAxis, titleStyle,
|
||||
PALETTE, FONT, axisCommon, standalonePeriodAxis, titleStyle,
|
||||
financialViewDisclosure, financialViewLabel,
|
||||
lineSeries, standardLegend, footnote, watermarks, LAYOUT,
|
||||
} from '../theme.js';
|
||||
|
||||
@@ -15,15 +16,15 @@ import {
|
||||
*/
|
||||
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)}%`;
|
||||
|
||||
return {
|
||||
backgroundColor: '#fff',
|
||||
title: {
|
||||
...titleStyle,
|
||||
text: '泡泡玛特核心利润率(20H1-26H1)',
|
||||
subtext: '核心利润率 = 毛利率 − 销售费用率 − 管理费用率,剔除汇兑与投资等非主业扰动',
|
||||
text: `泡泡玛特核心利润率(${financialViewLabel(view)})`,
|
||||
subtext: ['核心利润率 = 毛利率 − 销售费用率 − 管理费用率,剔除汇兑与投资等非主业扰动', financialViewDisclosure(view)].filter(Boolean).join('\n'),
|
||||
},
|
||||
legend: standardLegend(['核心利润率'], { itemGap: 32 }),
|
||||
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)}%`,
|
||||
textStyle: { fontFamily: FONT },
|
||||
},
|
||||
xAxis: { ...periodAxis, data: col(rows, 'period') },
|
||||
xAxis: { ...standalonePeriodAxis, data: col(rows, 'period') },
|
||||
yAxis: {
|
||||
...axisCommon,
|
||||
type: 'value',
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { col } from '../csv.js';
|
||||
import {
|
||||
PALETTE, FINANCIAL_COLORS, FONT, axisCommon, periodAxis, titleStyle, legendStyle,
|
||||
PALETTE, FINANCIAL_COLORS, FONT, axisCommon, standalonePeriodAxis, titleStyle, legendStyle,
|
||||
financialViewDisclosure, financialViewLabel,
|
||||
footnote, watermarks, LAYOUT,
|
||||
} 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 core = col(rows, 'core_profit');
|
||||
const yoy = col(rows, 'yoy');
|
||||
@@ -18,8 +19,8 @@ export function ppmtCoreProfitChart(rows, { width, height }) {
|
||||
backgroundColor: '#fff',
|
||||
title: {
|
||||
...titleStyle,
|
||||
text: '泡泡玛特核心利润及同比增速(20H1-26H1)',
|
||||
subtext: '核心利润 = 毛利 - 销售费用 - 管理费用,单位:亿元',
|
||||
text: `泡泡玛特核心利润及同比增速(${financialViewLabel(view)})`,
|
||||
subtext: ['核心利润 = 毛利 - 销售费用 - 管理费用,单位:亿元', financialViewDisclosure(view)].filter(Boolean).join('\n'),
|
||||
},
|
||||
legend: {
|
||||
...legendStyle,
|
||||
@@ -49,7 +50,7 @@ export function ppmtCoreProfitChart(rows, { width, height }) {
|
||||
return result;
|
||||
},
|
||||
},
|
||||
xAxis: { ...periodAxis, data: periods },
|
||||
xAxis: { ...standalonePeriodAxis, data: periods },
|
||||
yAxis: [
|
||||
{
|
||||
// 左轴:核心利润(亿元)
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
import { col } from '../csv.js';
|
||||
import {
|
||||
PALETTE, FONT, axisCommon, periodAxis, titleStyle, legendStyle,
|
||||
PALETTE, FONT, axisCommon, standalonePeriodAxis, titleStyle, legendStyle,
|
||||
financialViewDisclosure, financialViewLabel,
|
||||
lineSeries, footnote, watermarks, LAYOUT,
|
||||
} from '../theme.js';
|
||||
|
||||
/** 泡泡玛特期间费用结构:观察销售投入、管理成本与费用率的规模效应。 */
|
||||
export function expenseStructureChart(rows, { width, height }) {
|
||||
export function expenseStructureChart(rows, { width, height, view = 'all' }) {
|
||||
return {
|
||||
backgroundColor: '#fff',
|
||||
title: {
|
||||
...titleStyle,
|
||||
text: '泡泡玛特费用结构与费用率(年度)',
|
||||
subtext: '销售、管理、财务费用;折线为四项费用率',
|
||||
text: `泡泡玛特费用结构与费用率(${financialViewLabel(view)})`,
|
||||
subtext: ['销售、管理、财务费用;折线为四项费用率', financialViewDisclosure(view)].filter(Boolean).join('\n'),
|
||||
},
|
||||
legend: {
|
||||
...legendStyle,
|
||||
@@ -25,7 +26,7 @@ export function expenseStructureChart(rows, { width, height }) {
|
||||
textStyle: { fontFamily: FONT },
|
||||
valueFormatter: (value) => value.toFixed(1),
|
||||
},
|
||||
xAxis: { ...periodAxis, data: col(rows, 'period') },
|
||||
xAxis: { ...standalonePeriodAxis, data: col(rows, 'period') },
|
||||
yAxis: [
|
||||
{
|
||||
...axisCommon,
|
||||
@@ -68,7 +69,7 @@ export function expenseStructureChart(rows, { width, height }) {
|
||||
],
|
||||
graphic: [
|
||||
...watermarks(width, height),
|
||||
footnote('数据来源:泡泡玛特合并利润表(年度口径)| @思考的Joey'),
|
||||
footnote('数据来源:泡泡玛特合并利润表;H2为单独半年 | @思考的Joey'),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { col } from '../csv.js';
|
||||
import {
|
||||
PALETTE, FONT, axisCommon, periodAxis, titleStyle, standardLegend,
|
||||
PALETTE, FONT, axisCommon, standalonePeriodAxis, titleStyle, standardLegend,
|
||||
financialViewDisclosure, financialViewLabel,
|
||||
lineSeries, footnote, watermarks, LAYOUT,
|
||||
} 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}%`;
|
||||
|
||||
return {
|
||||
backgroundColor: '#fff',
|
||||
title: {
|
||||
...titleStyle,
|
||||
text: '泡泡玛特营收到利润的增速对比(2020H1-2026H1)',
|
||||
subtext: '同比增速(%),净利增速显著高于营收增速体现利润弹性',
|
||||
text: `泡泡玛特营收到利润的增速对比(${financialViewLabel(view)})`,
|
||||
subtext: ['同比增速(%),净利增速显著高于营收增速体现利润弹性', financialViewDisclosure(view)].filter(Boolean).join('\n'),
|
||||
},
|
||||
legend: standardLegend(['非GAAP归母净利', '毛利润', '营业收入', '营业成本'], { itemGap: 34 }),
|
||||
grid: { left: 84, right: 56, top: LAYOUT.gridTop, bottom: 80 },
|
||||
@@ -60,7 +61,7 @@ export function ppmtGrowthChart(rows, { width, height }) {
|
||||
valueFormatter: (v) => `${v}%`,
|
||||
textStyle: { fontFamily: FONT },
|
||||
},
|
||||
xAxis: { ...periodAxis, data: col(rows, 'period') },
|
||||
xAxis: { ...standalonePeriodAxis, data: col(rows, 'period') },
|
||||
yAxis: {
|
||||
...axisCommon,
|
||||
type: 'value',
|
||||
@@ -105,7 +106,7 @@ export function ppmtGrowthChart(rows, { width, height }) {
|
||||
})),
|
||||
graphic: [
|
||||
...watermarks(width, height),
|
||||
footnote('数据来源:泡泡玛特财报,2020H1/H2非GAAP净利用归母净利替代 | @思考的Joey'),
|
||||
footnote('数据来源:泡泡玛特财报;2020H1/H2非GAAP净利用归母净利替代 | @思考的Joey'),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { col } from '../csv.js';
|
||||
import {
|
||||
PALETTE, FONT, axisCommon, categoryAxis, titleStyle, legendStyle,
|
||||
financialViewDisclosure, financialViewLabel, shortStandalonePeriod,
|
||||
lineSeries, footnote, watermarks, LAYOUT,
|
||||
} from '../theme.js';
|
||||
|
||||
@@ -8,7 +9,7 @@ import {
|
||||
* 泡泡玛特主要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');
|
||||
|
||||
@@ -42,8 +43,8 @@ export function ipChart(rows, { width, height }) {
|
||||
backgroundColor: '#fff',
|
||||
title: {
|
||||
...titleStyle,
|
||||
text: '泡泡玛特主要IP收入趋势(2021H1-2025H2)',
|
||||
subtext: '单位:亿元',
|
||||
text: `泡泡玛特主要IP收入趋势(${financialViewLabel(view)})`,
|
||||
subtext: ['单位:亿元;H2为单独半年', financialViewDisclosure(view)].filter(Boolean).join('\n'),
|
||||
},
|
||||
legend: {
|
||||
...legendStyle,
|
||||
@@ -62,7 +63,7 @@ export function ipChart(rows, { width, height }) {
|
||||
data: periods,
|
||||
axisLabel: {
|
||||
...categoryAxis.axisLabel,
|
||||
formatter: (value) => String(value).replace(/^20/, ''),
|
||||
formatter: shortStandalonePeriod,
|
||||
},
|
||||
},
|
||||
yAxis: {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { col } from '../csv.js';
|
||||
import {
|
||||
PALETTE, FONT, axisCommon, categoryAxis, titleStyle, labelStyle,
|
||||
PALETTE, FONT, axisCommon, standalonePeriodAxis, titleStyle, labelStyle,
|
||||
financialViewDisclosure, financialViewLabel,
|
||||
barSeries, lineSeries, standardLegend, footnote, watermarks, LAYOUT,
|
||||
} from '../theme.js';
|
||||
|
||||
@@ -34,13 +35,13 @@ const BAR_COLOR = '#176B52'; // 墨翡翠
|
||||
const LINE_COLOR = '#7A3FE0'; // 鲜亮紫
|
||||
const LINE_ON_BAR = '#DCC9FF'; // 压在翡翠柱面上时用的浅紫(对比度约 6:1)
|
||||
|
||||
const Y_MAX = 160; // 左轴上限
|
||||
const YOY_MIN = -50; // 右轴下限
|
||||
const YOY_MAX = 150; // 右轴上限
|
||||
|
||||
export function ppmtIpExMonstersChart(rows, { width, height }) {
|
||||
export function ppmtIpExMonstersChart(rows, { width, height, view = 'all' }) {
|
||||
const periods = col(rows, 'period');
|
||||
const revenue = col(rows, 'revenue');
|
||||
const yMax = Math.max(...revenue) > 160 ? 300 : 160;
|
||||
// 前两期无同比基数,CSV 里留空;parseCSV 会给出 '',统一转成 null
|
||||
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 yoyData = yoy.map((v, i) => {
|
||||
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 onBar = lineFrac + labelGap < barFrac;
|
||||
@@ -61,8 +62,8 @@ export function ppmtIpExMonstersChart(rows, { width, height }) {
|
||||
backgroundColor: '#fff',
|
||||
title: {
|
||||
...titleStyle,
|
||||
text: '泡泡玛特除 THE MONSTERS 外的IP收入及同比(21H1-26H1)',
|
||||
subtext: 'IP收入合计 − THE MONSTERS,单位:亿元;H2 为下半年单期,非全年累计',
|
||||
text: `泡泡玛特除 THE MONSTERS 外的IP收入及同比(${financialViewLabel(view)})`,
|
||||
subtext: ['IP收入合计 − THE MONSTERS,单位:亿元;H2为下半年单期,非全年累计', financialViewDisclosure(view)].filter(Boolean).join('\n'),
|
||||
},
|
||||
legend: standardLegend(['除MONSTERS外收入', '同比增速'], { itemGap: 32 }),
|
||||
grid: { left: 78, right: 128, top: LAYOUT.gridTop, bottom: 90 },
|
||||
@@ -82,12 +83,7 @@ export function ppmtIpExMonstersChart(rows, { width, height }) {
|
||||
return out;
|
||||
},
|
||||
},
|
||||
xAxis: {
|
||||
...categoryAxis,
|
||||
data: periods,
|
||||
// 只去世纪前缀:这张表的 H2 是单半年,不能简写成「全年」
|
||||
axisLabel: { ...categoryAxis.axisLabel, formatter: (v) => v.replace(/^20/, '') },
|
||||
},
|
||||
xAxis: { ...standalonePeriodAxis, data: periods },
|
||||
yAxis: [
|
||||
{
|
||||
...axisCommon,
|
||||
@@ -97,9 +93,8 @@ export function ppmtIpExMonstersChart(rows, { width, height }) {
|
||||
nameGap: 50,
|
||||
nameTextStyle: { color: PALETTE.grey, fontSize: 13, fontFamily: FONT },
|
||||
min: 0,
|
||||
// 定死 160:留出顶部余量,最高柱(139.0)不会顶到绘图区上沿
|
||||
max: Y_MAX,
|
||||
interval: 20,
|
||||
max: yMax,
|
||||
interval: yMax === 160 ? 20 : 50,
|
||||
},
|
||||
{
|
||||
type: 'value',
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import {
|
||||
PALETTE, FONT, axisCommon, categoryAxis, titleStyle, LEGEND_ITEM_WIDTH,
|
||||
financialViewDisclosure, financialViewLabel, shortStandalonePeriod,
|
||||
barSeries, footnote, watermarks, LAYOUT,
|
||||
} from '../theme.js';
|
||||
|
||||
@@ -38,7 +39,7 @@ const COLORS = {
|
||||
const LEGEND_WIDTH = 160;
|
||||
const LEGEND_RIGHT = 20;
|
||||
|
||||
export function ppmtIpShareHistoryChart(rows, { width, height }) {
|
||||
export function ppmtIpShareHistoryChart(rows, { width, height, view = 'all' }) {
|
||||
const periods = Object.keys(rows[0]).filter((k) => k !== 'ip');
|
||||
const names = rows.map((r) => r.ip);
|
||||
const totals = periods.map((p) => rows.reduce((s, r) => s + r[p], 0));
|
||||
@@ -49,8 +50,8 @@ export function ppmtIpShareHistoryChart(rows, { width, height }) {
|
||||
backgroundColor: '#fff',
|
||||
title: {
|
||||
...titleStyle,
|
||||
text: '泡泡玛特各IP半年收入占比(21H1-26H1)',
|
||||
subtext: '占当期IP收入合计的比重;H2 为下半年单期,非全年累计',
|
||||
text: `泡泡玛特各IP收入占比(${financialViewLabel(view)})`,
|
||||
subtext: ['占当期IP收入合计的比重;H2为下半年单期,非全年累计', financialViewDisclosure(view)].filter(Boolean).join('\n'),
|
||||
},
|
||||
legend: {
|
||||
orient: 'vertical',
|
||||
@@ -89,7 +90,7 @@ export function ppmtIpShareHistoryChart(rows, { width, height }) {
|
||||
...categoryAxis,
|
||||
data: periods,
|
||||
// 只去世纪前缀:这张表的 H2 是单半年,不能简写成「全年」
|
||||
axisLabel: { ...categoryAxis.axisLabel, formatter: (v) => v.replace(/^20/, '') },
|
||||
axisLabel: { ...categoryAxis.axisLabel, formatter: shortStandalonePeriod },
|
||||
},
|
||||
yAxis: {
|
||||
...axisCommon,
|
||||
|
||||
@@ -1,19 +1,20 @@
|
||||
import { col } from '../csv.js';
|
||||
import {
|
||||
PALETTE, FINANCIAL_COLORS, FONT, axisCommon, periodAxis, titleStyle, legendStyle,
|
||||
PALETTE, FINANCIAL_COLORS, FONT, axisCommon, standalonePeriodAxis, titleStyle, legendStyle,
|
||||
financialViewLabel,
|
||||
lineDataWithSafeLabels, lineSeries, barSeries, footnote, watermarks, LAYOUT,
|
||||
} from '../theme.js';
|
||||
|
||||
/** 泡泡玛特现金、负债与净营运资本:观察流动性与资金占用。 */
|
||||
export function liquidityWorkingCapitalChart(rows, { width, height }) {
|
||||
export function liquidityWorkingCapitalChart(rows, { width, height, view = 'all' }) {
|
||||
const periods = col(rows, 'period');
|
||||
|
||||
return {
|
||||
backgroundColor: '#fff',
|
||||
title: {
|
||||
...titleStyle,
|
||||
text: '泡泡玛特现金、负债与净营运资本',
|
||||
subtext: '净营运资本 = 流动资产 − 流动负债;单位:亿元',
|
||||
text: `泡泡玛特现金、负债与净营运资本(${financialViewLabel(view)})`,
|
||||
subtext: '净营运资本 = 流动资产 − 流动负债;单位:亿元;H2 为年末时点',
|
||||
},
|
||||
legend: {
|
||||
...legendStyle,
|
||||
@@ -27,7 +28,7 @@ export function liquidityWorkingCapitalChart(rows, { width, height }) {
|
||||
textStyle: { fontFamily: FONT },
|
||||
valueFormatter: (value) => `${value.toFixed(1)} 亿元`,
|
||||
},
|
||||
xAxis: { ...periodAxis, data: periods },
|
||||
xAxis: { ...standalonePeriodAxis, data: periods },
|
||||
yAxis: {
|
||||
...axisCommon,
|
||||
type: 'value',
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { col } from '../csv.js';
|
||||
import {
|
||||
PALETTE, FONT, axisCommon, periodAxis, titleStyle, standardLegend,
|
||||
PALETTE, FONT, axisCommon, standalonePeriodAxis, titleStyle, standardLegend,
|
||||
financialViewDisclosure, financialViewLabel,
|
||||
lineSeries, footnote, watermarks, LAYOUT,
|
||||
} 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}%`;
|
||||
|
||||
return {
|
||||
backgroundColor: '#fff',
|
||||
title: {
|
||||
...titleStyle,
|
||||
text: '泡泡玛特利润率与费用率拆解(2020H1-2026H1)',
|
||||
subtext: '毛利率抬升叠加费用率下降,共同推动净利率走高(H1为半年,H2为全年累计)',
|
||||
text: `泡泡玛特利润率与费用率拆解(${financialViewLabel(view)})`,
|
||||
subtext: ['毛利率抬升叠加费用率下降,共同推动净利率走高', financialViewDisclosure(view)].filter(Boolean).join('\n'),
|
||||
},
|
||||
legend: standardLegend(SERIES.map((series) => series.name)),
|
||||
grid: { left: 78, right: 56, top: LAYOUT.gridTop, bottom: 80 },
|
||||
@@ -71,7 +72,7 @@ export function ppmtMarginsChart(rows, { width, height }) {
|
||||
valueFormatter: (v) => `${v}%`,
|
||||
textStyle: { fontFamily: FONT },
|
||||
},
|
||||
xAxis: { ...periodAxis, data: col(rows, 'period') },
|
||||
xAxis: { ...standalonePeriodAxis, data: col(rows, 'period') },
|
||||
yAxis: {
|
||||
...axisCommon,
|
||||
type: 'value',
|
||||
|
||||
+12
-11
@@ -1,6 +1,7 @@
|
||||
import { col } from '../csv.js';
|
||||
import {
|
||||
PALETTE, FONT, axisCommon, periodAxis, titleStyle, legendStyle,
|
||||
PALETTE, FONT, axisCommon, standalonePeriodAxis, titleStyle, legendStyle,
|
||||
financialViewDisclosure, financialViewLabel,
|
||||
lineSeries, footnote, watermarks, LAYOUT,
|
||||
} from '../theme.js';
|
||||
|
||||
@@ -8,19 +9,19 @@ import {
|
||||
* 泡泡玛特盈利能力分析图
|
||||
* 柱状图展示收入和利润,折线图展示毛利率
|
||||
*/
|
||||
export function profitChart(rows, { width, height }) {
|
||||
const periods = col(rows, 'period').reverse(); // 按时间正序
|
||||
const revenue = col(rows, 'revenue').reverse();
|
||||
const grossProfit = col(rows, 'gross_profit').reverse();
|
||||
const netProfit = col(rows, 'net_profit').reverse();
|
||||
const grossMargin = col(rows, 'gross_margin').reverse();
|
||||
export function profitChart(rows, { width, height, view = 'all' }) {
|
||||
const periods = col(rows, 'period');
|
||||
const revenue = col(rows, 'revenue');
|
||||
const grossProfit = col(rows, 'gross_profit');
|
||||
const netProfit = col(rows, 'net_profit');
|
||||
const grossMargin = col(rows, 'gross_margin');
|
||||
|
||||
return {
|
||||
backgroundColor: '#fff',
|
||||
title: {
|
||||
...titleStyle,
|
||||
text: '泡泡玛特盈利能力分析(2022H2-2026H1)',
|
||||
subtext: '营收、毛利、净利润及毛利率变化',
|
||||
text: `泡泡玛特盈利能力分析(${financialViewLabel(view)})`,
|
||||
subtext: ['营收、毛利、净利润及毛利率变化', financialViewDisclosure(view)].filter(Boolean).join('\n'),
|
||||
},
|
||||
legend: {
|
||||
...legendStyle,
|
||||
@@ -33,7 +34,7 @@ export function profitChart(rows, { width, height }) {
|
||||
axisPointer: { type: 'shadow' },
|
||||
textStyle: { fontFamily: FONT },
|
||||
},
|
||||
xAxis: { ...periodAxis, data: periods },
|
||||
xAxis: { ...standalonePeriodAxis, data: periods },
|
||||
yAxis: [
|
||||
{
|
||||
...axisCommon,
|
||||
@@ -122,7 +123,7 @@ export function profitChart(rows, { width, height }) {
|
||||
],
|
||||
graphic: [
|
||||
...watermarks(width, height),
|
||||
footnote('数据来源:泡泡玛特财报(累计数据)| @思考的Joey'),
|
||||
footnote('数据来源:泡泡玛特财报;H2为单独半年 | @思考的Joey'),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user