import { col } from '../csv.js';
import {
PALETTE, FONT, axisCommon, categoryAxis, titleStyle, labelStyle,
barSeries, lineSeries, standardLegend, footnote, watermarks, LAYOUT,
} from '../theme.js';
/**
* 泡泡玛特除 THE MONSTERS 外的 IP 收入及同比(2021H1-2026H1)
* 柱 = 各期 IP 收入合计 − THE MONSTERS,折线 = 同比增速。
* 看的是「剔掉唯一爆款后,其余 IP 盘子还在不在长」。
*
* 源:ppmt.xlsx「IP收入和占比」收入区的「计算合计」行减去 THE MONSTERS 行。
*
* ⚠ 这张表的 H2 是【单半年】,不是全年累计(已核对 2025H2 的 232.44
* = 利润表 371.2 − 138.76)。两个后果:
* 1. 轴标签只去世纪前缀(25H2),不能用 `periodAxis`(它会渲染成「25全年」)
* 2. 同比是「本期 vs 去年同半年」,即隔两列相除;前两期无基数,留空
*
* CSV 收入存两位小数:同比对基数的舍入敏感,一位小数在 16~20 亿的基数上
* 会让同比偏掉约 0.5pct。实测两位小数复算同比与全精度差 ≤ 0.02pct。
*
* 配色:墨翡翠柱 + 鲜亮紫线,两色 ΔE 隔着老远。
* 翡翠选得比紧邻堆叠图里 DIMOO 那支(#3E9E6E)更深,ΔE 22.7,不会被读成同一个色;
* 同时深到白字对比度 6.44:1,够柱内白色标签用。
*
* 标签避让(不加底色,白色药丸底太丑):
* 1. 柱子的数值放柱内白字(insideTop)——腾开柱顶上方,不和同比标签抢位置
* 2. 同比标签逐点判断:算出标签底边和柱顶的相对位置,只有整体压在柱面上的
* 那个点(26H1,线在 45%、柱顶在 80%)改用浅紫字,其余保持线色
*
* 浅紫而非白色:柱子自己的数值已经是白字,同比再用白字两者会混。
*/
const BAR_COLOR = '#176B52'; // 墨翡翠
const LINE_COLOR = '#7A3FE0'; // 鲜亮紫
const LINE_ON_BAR = '#DCC9FF'; // 压在翡翠柱面上时用的浅紫(对比度约 6:1)
const Y_MAX = 160; // 左轴上限
const YOY_MIN = -50; // 右轴下限
const YOY_MAX = 150; // 右轴上限
export function ppmtIpExMonstersChart(rows, { width, height }) {
const periods = col(rows, 'period');
const revenue = col(rows, 'revenue');
// 前两期无同比基数,CSV 里留空;parseCSV 会给出 '',统一转成 null
const yoy = col(rows, 'yoy').map((v) => (v === '' ? null : v));
// 逐点判断同比标签会不会压在柱面上。`label.position` 不接受函数(见 CLAUDE.md),
// 所以只能把 data 写成对象、逐点覆盖 color。
const plotH = height - LAYOUT.gridTop - 90; // 与下面 grid 的 top/bottom 一致
const labelGap = (8 + 13) / plotH; // labelStyle 的 distance + 字高,换成占比
const yoyData = yoy.map((v, i) => {
if (v === null) return null;
const barFrac = revenue[i] / Y_MAX;
const lineFrac = (v - YOY_MIN) / (YOY_MAX - YOY_MIN);
// 标签整体(底边到顶边)都落在柱子高度以内 → 压在柱面上
const onBar = lineFrac + labelGap < barFrac;
return onBar ? { value: v, label: { color: LINE_ON_BAR } } : v;
});
return {
backgroundColor: '#fff',
title: {
...titleStyle,
text: '泡泡玛特除 THE MONSTERS 外的IP收入及同比(21H1-26H1)',
subtext: 'IP收入合计 − THE MONSTERS,单位:亿元;H2 为下半年单期,非全年累计',
},
legend: standardLegend(['除MONSTERS外收入', '同比增速'], { itemGap: 32 }),
grid: { left: 78, right: 128, top: LAYOUT.gridTop, bottom: 90 },
tooltip: {
trigger: 'axis',
axisPointer: { type: 'cross' },
textStyle: { fontFamily: FONT },
formatter: (ps) => {
let out = `${ps[0].axisValue}
`;
ps.forEach((p) => {
if (p.seriesName === '除MONSTERS外收入') {
out += `${p.marker}${p.seriesName}: ${p.value.toFixed(1)} 亿元
`;
} else if (p.value !== null && p.value !== '') {
out += `${p.marker}${p.seriesName}: ${p.value > 0 ? '+' : ''}${p.value.toFixed(1)}%
`;
}
});
return out;
},
},
xAxis: {
...categoryAxis,
data: periods,
// 只去世纪前缀:这张表的 H2 是单半年,不能简写成「全年」
axisLabel: { ...categoryAxis.axisLabel, formatter: (v) => v.replace(/^20/, '') },
},
yAxis: [
{
...axisCommon,
type: 'value',
name: '收入(亿元)',
nameLocation: 'middle',
nameGap: 50,
nameTextStyle: { color: PALETTE.grey, fontSize: 13, fontFamily: FONT },
min: 0,
// 定死 160:留出顶部余量,最高柱(139.0)不会顶到绘图区上沿
max: Y_MAX,
interval: 20,
},
{
type: 'value',
name: '同比增速(%)',
nameLocation: 'middle',
nameGap: 60,
nameTextStyle: { color: PALETTE.grey, fontSize: 13, fontFamily: FONT },
min: YOY_MIN,
max: YOY_MAX,
interval: 50,
axisLine: { show: false },
axisTick: { show: false },
splitLine: { show: false },
axisLabel: { color: PALETTE.grey, fontSize: 12, fontFamily: FONT, formatter: '{value}%' },
},
],
series: [
barSeries({
name: '除MONSTERS外收入',
data: revenue,
color: BAR_COLOR,
barWidth: 34,
yAxisIndex: 0,
// 数值放柱内白字:腾开柱顶上方,避免和同比线的标签抢位置
label: {
show: true,
position: 'insideTop',
color: '#fff',
fontFamily: FONT,
fontSize: 11,
fontWeight: 500,
formatter: (p) => p.value.toFixed(1),
overflow: 'none', // 矮柱也要显示,见 CLAUDE.md
},
}),
{
// lineSeries() 没有 ...rest,yAxisIndex 只能展开后再补
...lineSeries({ name: '同比增速', data: yoyData, color: LINE_COLOR }),
yAxisIndex: 1,
label: {
...labelStyle(LINE_COLOR, 'top'),
formatter: (p) => (p.value === null || p.value === '' ? ''
: `${p.value > 0 ? '+' : ''}${p.value.toFixed(1)}%`),
},
},
],
graphic: [
...watermarks(width, height),
footnote('除 THE MONSTERS 外其余 IP 合计;同比对去年同半年 | @思考的Joey'),
],
};
}