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 `${items[i]}
${values[i].toFixed(2)}%`; const sign = types[i] === 'pos' ? '+' : '−'; return `${items[i]}
${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' ), ], }; }