Files
smart-fin-chart/js/charts/ppmt_region_share.js
T
2026-09-09 15:31:05 +08:00

112 lines
4.3 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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知行记'),
],
};
}