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 = `${ps[0].axisValue} 合计 ${totals[i].toFixed(1)} 亿元
`; 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('
'); }, }, 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'), ], }; }