feat: 构建泡泡玛特相关图表
This commit is contained in:
@@ -0,0 +1,125 @@
|
||||
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'),
|
||||
],
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user