feat: 构建泡泡玛特相关图表

This commit is contained in:
Joey
2026-08-23 21:23:41 +08:00
commit 3ae396776d
31 changed files with 3454 additions and 0 deletions
+187
View File
@@ -0,0 +1,187 @@
import {
PALETTE, FONT, axisCommon, categoryAxis, titleStyle,
lineSeries, footnote, watermarks,
} from '../theme.js';
/** 圆角卡片:ECharts 的 graphic.group + rect + 多段文字 */
function infoCard({ left, top, width, title, lines, fill, stroke, titleColor }) {
const children = [
{
type: 'rect',
shape: { x: 0, y: 0, width, height: 108, r: 8 },
style: { fill, stroke, lineWidth: 1 },
},
{
type: 'text',
style: {
x: 20, y: 20,
text: title,
fill: titleColor,
fontSize: 15,
fontWeight: 'bold',
fontFamily: FONT,
},
},
];
lines.forEach((line, i) => {
children.push({
type: 'text',
style: {
x: 20, y: 52 + i * 22,
text: line,
fill: '#555',
fontSize: 12.5,
fontFamily: FONT,
},
});
});
return { type: 'group', left, top, silent: true, children };
}
/**
* 图 5:人民币兑美元两种口径对比
* 只有两个时间点,重点在文字说明和端点标注,不在曲线本身
*/
export function fxChart(rows, { width, height }) {
const stages = rows.map((r) => r.stage);
const mid = rows.map((r) => r.mid_price);
const spot = rows.map((r) => r.onshore_spot);
// label.position 不接受函数(写函数会被静默忽略,退化成默认位置),
// 想让首尾点位置不同只能逐点覆盖:起点标在上方,末点标在右侧
const endpointLabels = (values) =>
values.map((v, i) => ({
value: v,
label: { position: i === values.length - 1 ? 'right' : 'top' },
}));
// 偏离点数 = (中间价 - 即期)× 10000
const gapStart = Math.round((mid[0] - spot[0]) * 10000);
const gapEnd = Math.round((mid[1] - spot[1]) * 10000);
const midGain = Math.round((mid[0] - mid[1]) * 10000);
const spotGain = Math.round((spot[0] - spot[1]) * 10000);
const midPct = (((mid[0] - mid[1]) / mid[0]) * 100).toFixed(1);
const spotPct = (((spot[0] - spot[1]) / spot[0]) * 100).toFixed(1);
const cardWidth = 340;
const cardTop = 132;
return {
backgroundColor: '#fff',
title: {
...titleStyle,
text: '人民币兑美元的两种「升值」口径:中间价 vs 在岸即期',
subtext: '口径区别在「谁定的价」:中间价是央行每日公布的锚,即期是市场在锚附近交易出来的成交价',
top: 20,
},
grid: { left: 96, right: 110, top: 340, bottom: 88 },
tooltip: { trigger: 'axis', textStyle: { fontFamily: FONT } },
xAxis: {
...categoryAxis,
boundaryGap: ['12%', '12%'],
data: stages,
},
yAxis: {
...axisCommon,
type: 'value',
name: 'USDCNY(数值越小=人民币越强)',
nameLocation: 'middle',
nameGap: 62,
nameTextStyle: { color: PALETTE.grey, fontSize: 13, fontFamily: FONT },
min: 6.75,
max: 7.05,
interval: 0.1,
axisLabel: {
...axisCommon.axisLabel,
formatter: (v) => v.toFixed(2),
},
},
series: [
{
...lineSeries({
name: '中间价',
data: mid,
color: '#3b6ea5',
formatter: (p) => p.value.toFixed(4),
}),
data: endpointLabels(mid),
symbolSize: 11,
itemStyle: { color: '#3b6ea5' },
label: {
show: true,
fontSize: 15,
fontWeight: 'bold',
color: '#3b6ea5',
fontFamily: FONT,
distance: 10,
formatter: (p) => p.value.toFixed(4),
},
},
{
...lineSeries({
name: '在岸即期',
data: spot,
color: PALETTE.coral,
formatter: (p) => p.value.toFixed(4),
}),
data: endpointLabels(spot),
symbolSize: 11,
itemStyle: { color: PALETTE.coral },
label: {
show: true,
fontSize: 15,
fontWeight: 'bold',
color: PALETTE.coral,
fontFamily: FONT,
distance: 10,
formatter: (p) => p.value.toFixed(4),
},
},
],
graphic: [
...watermarks(width, height),
infoCard({
left: 88, top: cardTop, width: cardWidth,
title: '中间价(央行公布的「锚」)',
lines: [
'每交易日 9:15 由央行公布:做市商报价',
'加权+逆周期因子,是当日波动中心与政策信号',
],
fill: '#eef4fa', stroke: '#c7dcf0', titleColor: '#2f5f8f',
}),
infoCard({
left: 88 + cardWidth + 24, top: cardTop, width: cardWidth,
title: '在岸即期(市场成交价)',
lines: [
'银行间外汇市场实时成交形成,16:30 收盘价',
'最常被引用;每日只能在中间价 ±2% 区间内波动',
],
fill: '#fdf1ed', stroke: '#f5d3c8', titleColor: '#c05a45',
}),
// 图例改成两行文字说明,信息量比色块图例大
{
type: 'text',
left: 108, top: 272, silent: true,
style: {
text: `● 中间价:${mid[0]}${mid[1]},累计升值 ${midGain}点(+${midPct}%`,
fill: '#3b6ea5', fontSize: 14, fontFamily: FONT,
},
},
{
type: 'text',
left: 108, top: 296, silent: true,
style: {
text: `● 在岸即期:${spot[0]}${spot[1]},累计升值 ${spotGain}点(+${spotPct}%`,
fill: PALETTE.coral, fontSize: 14, fontFamily: FONT,
},
},
footnote(
`注:涨幅差${midGain - spotGain}点=期初偏离${gapStart}−期末偏离${gapEnd}(即期偏强收窄)|` +
'汇兑重估用市场汇率而非中间价|数据:新华财经|Joey 谨制'
),
],
};
}