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

123 lines
3.9 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 { 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'
),
],
};
}