import { PALETTE, FONT, titleStyle, footnote, watermarks, } from '../theme.js'; /** * 泡泡玛特主要 IP 收入占比(2026H1) * 饼图(非环图),图例放在右侧:每项上行 IP 名、下行小字显示收入和占比。 * 扇区外直接标注 IP 名 + 数值(两行)。源:ppmt.xlsx「IP收入和占比」2026H1 列, * HACIPUPU、BUNNY、PUCKY 等该期无数据的 IP 不列入。 * 占比为衍生指标:各 IP 收入 ÷ 全部列入 IP 收入合计(源表占比列未填)。 * * 配色:手写的一条色相渐变谱(红→橙→金→橄榄→翡翠→青→蓝→紫), * 末位「外采及其他」用中性灰(残差项惯例)。冷色段靠明暗交替拉开 * (深蓝 → 浅蓝紫 → 紫 → 浅兰紫),避免同色系挤在一起分不清。 * 相邻扇区及全组两两 ΔE ≥ 24.6,与同页 ppmt_margins 四色及 wine ΔE ≥ 25。 */ const COLORS = [ '#D64545', // THE MONSTERS 玫红 '#E8913C', // 星星人 琥珀橙 '#D9C043', // 授权IP(非独家) 金 '#7FA83C', // CRYBABY 橄榄绿 '#3E9E6E', // DIMOO 翡翠 '#3FB6D0', // SKULLPANDA 亮青 '#2E5FA8', // 其他艺术家IP 深蓝 '#8E86E0', // HIRONO-小野 浅蓝紫 '#8A3FA8', // MOLLY 紫 '#D081C4', // Zsiga 浅兰紫 '#8C8C8C', // 外采及其他 中性灰 ]; export function ppmtIpShareChart(rows, { width, height }) { const data = rows.map((row, i) => ({ name: row.ip, value: row.revenue, itemStyle: { color: COLORS[i % COLORS.length] }, })); const total = data.reduce((s, d) => s + d.value, 0); const byName = Object.fromEntries(data.map((d) => [d.name, d])); // 把「饼图 + 两侧外标签」当成一个整体,与图例并成一组居中摆放: // 图例紧跟在右侧标签外缘之后,不会被推到画布边缘留下大片空白 const LEGEND_WIDTH = 152; // 最宽一行「17.7 亿元 · 10.3%」约 120px + 色块 const LEGEND_GAP = 24; // 右侧标签外缘 → 图例 // 左右标签占位不对称:左侧文字向左延伸(text-anchor: end)要占满, // 右侧只需容下引导线 + 首行文字起段,留太多会在图例前空出一大块 const LABEL_LEFT = 135; const LABEL_RIGHT = 72; const topPad = 78; const bottomPad = 64; const availH = height - topPad - bottomPad; // 半径取高/宽两个方向的较小值;上下各留 35px 给最上、最下扇区的两行标签 // 末尾 ×0.85:实测 250 偏大,缩 15% 留出呼吸感 const rByHeight = availH / 2 - 35; const rByWidth = (width - 48 - LEGEND_GAP - LEGEND_WIDTH - LABEL_LEFT - LABEL_RIGHT) / 2; const radius = Math.max(80, Math.min(rByHeight, rByWidth, 250) * 0.85); const blockW = radius * 2 + LABEL_LEFT + LABEL_RIGHT; const leftMargin = Math.max(16, (width - blockW - LEGEND_GAP - LEGEND_WIDTH) / 2); const centerX = leftMargin + LABEL_LEFT + radius; const centerY = topPad + availH / 2; const legendLeft = leftMargin + blockW + LEGEND_GAP; return { backgroundColor: '#fff', title: { ...titleStyle, text: '泡泡玛特主要IP收入占比(2026H1)', subtext: `占比 = 各IP收入 ÷ 全部IP收入合计(合计 ${total.toFixed(1)} 亿元),单位:亿元`, }, // 图例在右侧承载 IP 名 + 收入/占比明细 legend: { orient: 'vertical', left: legendLeft, width: LEGEND_WIDTH, top: 'middle', itemGap: 14, itemWidth: 14, itemHeight: 14, align: 'left', data: data.map((d) => d.name), formatter: (name) => { const d = byName[name]; const pct = (d.value / total) * 100; return `{name|${name}}\n{sub|${d.value.toFixed(1)} 亿元 · ${pct.toFixed(1)}%}`; }, textStyle: { rich: { name: { fontFamily: FONT, fontSize: 13, fontWeight: 600, color: PALETTE.ink, lineHeight: 18 }, sub: { fontFamily: FONT, fontSize: 11, color: PALETTE.grey, lineHeight: 16 }, }, }, }, tooltip: { trigger: 'item', textStyle: { fontFamily: FONT }, formatter: (p) => `${p.marker}${p.name}:${p.value.toFixed(1)} 亿元(${p.percent.toFixed(1)}%)`, }, series: [ { name: 'IP收入占比', type: 'pie', radius, center: [centerX, centerY], data, label: { show: true, formatter: (p) => `{name|${p.name}}\n{value|${p.value.toFixed(1)} 亿元}`, rich: { name: { fontFamily: FONT, fontSize: 12, color: PALETTE.ink, fontWeight: 600, lineHeight: 16 }, value: { fontFamily: FONT, fontSize: 11, color: PALETTE.grey, lineHeight: 15 }, }, }, labelLine: { length: 14, length2: 16, lineStyle: { color: PALETTE.axisLine } }, itemStyle: { borderColor: '#fff', borderWidth: 2 }, emphasis: { itemStyle: { shadowBlur: 12, shadowColor: 'rgba(0,0,0,0.2)' }, }, }, ], graphic: [ ...watermarks(width, height), footnote('HACIPUPU、BUNNY、PUCKY 等 IP 2026H1 无收入数据,未列入 | @思考的Joey'), ], }; }