126 lines
4.2 KiB
JavaScript
126 lines
4.2 KiB
JavaScript
import {
|
||
PALETTE, FONT, axisCommon, categoryAxis, titleStyle,
|
||
barSeries, footnote, watermarks, LAYOUT,
|
||
} from '../theme.js';
|
||
|
||
/**
|
||
* 泡泡玛特各 IP 半年收入占比(2021H1-2026H1)
|
||
* 归一化堆叠柱状图:每根柱固定 100%,看的是收入结构的变化而非规模。
|
||
* 源:ppmt.xlsx「IP收入和占比」的收入区;该表占比区未填,占比由收入算出。
|
||
*
|
||
* ⚠ 这张表的 H2 是【单半年】,不是全年累计——已核对 2025H2 的 232.44
|
||
* = 利润表 371.2 − 138.76。所以轴标签只去世纪前缀(25H2),
|
||
* 不能用 theme 的 `periodAxis`(它会渲染成「25全年」,对这张图是错的)。
|
||
*
|
||
* CSV 存两位小数:归一化占比对小值敏感,一位小数会让早期小 IP 失真
|
||
* (如 CRYBABY 2023H1 的 0.27 记成 0.3,相对误差 13%)。
|
||
*
|
||
* 18 行源数据里 8 个已停单列或体量极小的 IP 合并成「其他IP(含已停单列)」,
|
||
* 合并后仍是 11 个系列、逐期加总恰好 100%。
|
||
*
|
||
* 配色按 IP 名与同页饼图 ppmt_ip_share.js 一一对应,便于两图互相对照;
|
||
* 合并桶沿用 Zsiga 原色(Zsiga 已并入其中)。
|
||
*/
|
||
const COLORS = {
|
||
'THE MONSTERS': '#D64545',
|
||
'星星人': '#E8913C',
|
||
'授权IP(非独家)': '#D9C043',
|
||
'CRYBABY': '#7FA83C',
|
||
'DIMOO': '#3E9E6E',
|
||
'SKULLPANDA': '#3FB6D0',
|
||
'其他艺术家IP': '#2E5FA8',
|
||
'HIRONO-小野': '#8E86E0',
|
||
'MOLLY': '#8A3FA8',
|
||
'其他IP(含已停单列)': '#D081C4',
|
||
'外采及其他': '#8C8C8C',
|
||
};
|
||
|
||
const LEGEND_WIDTH = 160;
|
||
const LEGEND_RIGHT = 20;
|
||
|
||
export function ppmtIpShareHistoryChart(rows, { width, height }) {
|
||
const periods = Object.keys(rows[0]).filter((k) => k !== 'ip');
|
||
const names = rows.map((r) => r.ip);
|
||
const totals = periods.map((p) => rows.reduce((s, r) => s + r[p], 0));
|
||
// 归一化:各 IP 占当期合计的比重,逐期加总恰为 100%
|
||
const toPct = (r) => periods.map((p, i) => (r[p] / totals[i]) * 100);
|
||
|
||
return {
|
||
backgroundColor: '#fff',
|
||
title: {
|
||
...titleStyle,
|
||
text: '泡泡玛特各IP半年收入占比(21H1-26H1)',
|
||
subtext: '占当期IP收入合计的比重;H2 为下半年单期,非全年累计',
|
||
},
|
||
legend: {
|
||
orient: 'vertical',
|
||
right: LEGEND_RIGHT,
|
||
width: LEGEND_WIDTH,
|
||
top: 'middle',
|
||
itemGap: 12,
|
||
itemWidth: 14,
|
||
itemHeight: 14,
|
||
data: names,
|
||
textStyle: { color: PALETTE.ink, fontSize: 12, fontFamily: FONT },
|
||
},
|
||
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()
|
||
.filter((p) => p.value > 0)
|
||
.map((p) => `${p.marker}${p.seriesName}: ${p.value.toFixed(1)}%`
|
||
+ `(${((p.value / 100) * totals[i]).toFixed(1)} 亿元)`)
|
||
.join('<br/>');
|
||
},
|
||
},
|
||
grid: {
|
||
left: 78,
|
||
right: LEGEND_RIGHT + LEGEND_WIDTH + 20,
|
||
top: LAYOUT.gridTop,
|
||
bottom: 70,
|
||
},
|
||
xAxis: {
|
||
...categoryAxis,
|
||
data: periods,
|
||
// 只去世纪前缀:这张表的 H2 是单半年,不能简写成「全年」
|
||
axisLabel: { ...categoryAxis.axisLabel, formatter: (v) => v.replace(/^20/, '') },
|
||
},
|
||
yAxis: {
|
||
...axisCommon,
|
||
type: 'value',
|
||
min: 0,
|
||
max: 100,
|
||
interval: 20,
|
||
axisLabel: { ...axisCommon.axisLabel, formatter: '{value}%' },
|
||
},
|
||
series: rows.map((r) => barSeries({
|
||
name: r.ip,
|
||
data: toPct(r),
|
||
color: COLORS[r.ip] ?? PALETTE.grey,
|
||
barWidth: 40,
|
||
stack: 'share',
|
||
label: {
|
||
show: true,
|
||
position: 'inside',
|
||
color: '#fff',
|
||
fontSize: 11,
|
||
fontFamily: FONT,
|
||
fontWeight: 500,
|
||
// 低于 4% 的段塞不进字,留空靠 tooltip 读数
|
||
formatter: (p) => (p.value >= 4 ? p.value.toFixed(1) : ''),
|
||
},
|
||
emphasis: { focus: 'series' },
|
||
})),
|
||
graphic: [
|
||
...watermarks(width, height),
|
||
footnote('其他IP(含已停单列)= Zsiga、HACIPUPU、BUNNY、PUCKY、小甜豆等 | @思考的Joey'),
|
||
],
|
||
};
|
||
}
|