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

127 lines
5.1 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, 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'),
],
};
}