123 lines
3.9 KiB
JavaScript
123 lines
3.9 KiB
JavaScript
import { col } from '../csv.js';
|
||
import {
|
||
PALETTE, FONT, axisCommon, titleStyle, footnote, watermarks,
|
||
} from '../theme.js';
|
||
|
||
/**
|
||
* 净利率同比拆解瀑布图(2025H1 → 2026H1)
|
||
*
|
||
* ECharts 没有原生瀑布图,用「透明占位柱 + 可见柱」堆叠模拟:
|
||
* 占位柱撑到该项起点,可见柱只画增量部分。
|
||
*
|
||
* 口径要点:各项先算占营收比的税前变化,再乘税盾 (1-t₁) 折成对净利率的贡献,
|
||
* 税率变化单独成项。恒等式 净利率 = 税前利润率 × (1-税率),
|
||
* Δ净 = Δ税前×(1-t₁) − 税前₂×Δt,实测闭合误差 0.00pct。
|
||
*/
|
||
const COLOR = {
|
||
total: '#2F5C85', // 首尾锚点
|
||
neg: '#C0392B', // 拖累
|
||
pos: '#1A9E5C', // 利好
|
||
};
|
||
|
||
export function ppmtBridgeChart(rows, { width, height }) {
|
||
const items = col(rows, 'item');
|
||
const types = col(rows, 'type');
|
||
const bases = col(rows, 'base');
|
||
const values = col(rows, 'value');
|
||
|
||
return {
|
||
backgroundColor: '#fff',
|
||
title: {
|
||
...titleStyle,
|
||
text: '净利率同比拆解:33.7% → 29.7%(2025H1 → 2026H1)',
|
||
subtext: '单位:占营收百分点。汇兑等其他收益项是最大拖累,费用率改善仅部分对冲',
|
||
},
|
||
grid: { left: 114, right: 83, top: 120, bottom: 130 },
|
||
tooltip: {
|
||
trigger: 'axis',
|
||
axisPointer: { type: 'shadow' },
|
||
textStyle: { fontFamily: FONT },
|
||
formatter: (ps) => {
|
||
const p = ps.find((x) => x.seriesName === '变化');
|
||
if (!p) return '';
|
||
const i = p.dataIndex;
|
||
if (types[i] === 'total') return `<b>${items[i]}</b><br/>${values[i].toFixed(2)}%`;
|
||
const sign = types[i] === 'pos' ? '+' : '−';
|
||
return `<b>${items[i]}</b><br/>${sign}${values[i].toFixed(2)} pct`;
|
||
},
|
||
},
|
||
xAxis: {
|
||
type: 'category',
|
||
data: items,
|
||
axisLine: { show: true, lineStyle: { color: PALETTE.axisLine } },
|
||
axisTick: { show: false },
|
||
axisLabel: {
|
||
color: PALETTE.ink,
|
||
fontSize: 11,
|
||
fontFamily: FONT,
|
||
interval: 0,
|
||
rotate: 32,
|
||
margin: 12,
|
||
},
|
||
},
|
||
yAxis: {
|
||
...axisCommon,
|
||
type: 'value',
|
||
name: '净利率(%)',
|
||
nameLocation: 'middle',
|
||
nameGap: 52,
|
||
nameTextStyle: { color: PALETTE.grey, fontSize: 13, fontFamily: FONT },
|
||
min: 26,
|
||
max: 36,
|
||
interval: 2,
|
||
axisLabel: { ...axisCommon.axisLabel, formatter: '{value}%' },
|
||
},
|
||
series: [
|
||
{
|
||
// 占位柱:撑到每项起点,完全透明
|
||
name: '占位',
|
||
type: 'bar',
|
||
stack: 'bridge',
|
||
silent: true,
|
||
itemStyle: { color: 'transparent' },
|
||
emphasis: { itemStyle: { color: 'transparent' } },
|
||
data: bases,
|
||
},
|
||
{
|
||
name: '变化',
|
||
type: 'bar',
|
||
stack: 'bridge',
|
||
// 柱宽固定时 barCategoryGap 无效,间距只能靠 grid 左右边距调节:
|
||
// 步距 = 绘图宽 / 类目数,间隙 = 步距 − barWidth
|
||
barWidth: 51,
|
||
data: values.map((v, i) => ({
|
||
value: v,
|
||
itemStyle: { color: COLOR[types[i]] },
|
||
})),
|
||
label: {
|
||
show: true,
|
||
position: 'top',
|
||
fontFamily: FONT,
|
||
fontSize: 11,
|
||
fontWeight: 'bold',
|
||
color: PALETTE.ink,
|
||
formatter: (p) => {
|
||
const i = p.dataIndex;
|
||
if (types[i] === 'total') return `${values[i].toFixed(1)}%`;
|
||
// 所有项都显示(资产减值0.01、其他业务净额0.01也必须标)
|
||
const sign = types[i] === 'pos' ? '+' : '−';
|
||
return `${sign}${values[i].toFixed(2)}pct`;
|
||
},
|
||
},
|
||
},
|
||
],
|
||
graphic: [
|
||
...watermarks(width, height),
|
||
footnote(
|
||
'各项为占营收百分点,已折算为对净利率影响(×税盾系数)\n' +
|
||
'其他收益净额:26H1净损失6.89亿,主要为汇兑损失7.20亿(25H1为汇兑收益1.20亿) | @思考的Joey'
|
||
),
|
||
],
|
||
};
|
||
}
|