Files
smart-fin-chart/js/charts/ppmt_ip_share_history.js
T
2026-08-23 21:23:41 +08:00

126 lines
4.2 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,
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'),
],
};
}