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
+247
View File
@@ -0,0 +1,247 @@
/**
* 方木风格主题 —— 统一的配色、字体、坐标轴、水印
* 所有图表共用这一份配置,保证视觉一致性
*/
export const WATERMARK_TEXT = '@思考的Joey';
// 字体名用单引号:SVG 渲染时字符串会被塞进 style="..." 属性,
// 用双引号会提前闭合属性、生成非法 SVG(canvas 渲染看不出来,导出 SVG 时才炸)
export const FONT = "'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif";
/** 调色板:低饱和、克制,接近财经媒体常用的莫兰迪系 */
export const PALETTE = {
brown: '#6b4a2b',
sand: '#d5c7b4',
coral: '#ec7b73',
green: '#2f8a5b',
orange: '#d98324',
purple: '#9b4fa8',
blue: '#3b6ea5',
navy: '#1f4e79',
red: '#b8342a',
sage: '#8aab7e',
ink: '#333333',
grey: '#8c8c8c',
gridLine: '#e8e8e8',
axisLine: '#d0d0d0',
};
/** 坐标轴通用样式:只留浅色横向网格,去掉轴线的存在感 */
export const axisCommon = {
axisLine: { show: false },
axisTick: { show: false },
axisLabel: {
color: PALETTE.grey,
fontSize: 13,
fontFamily: FONT,
},
splitLine: {
show: true,
lineStyle: { color: PALETTE.gridLine, width: 1 },
},
};
/** X 轴:显示轴线,不显示网格 */
export const categoryAxis = {
type: 'category',
boundaryGap: true,
axisLine: { show: true, lineStyle: { color: PALETTE.axisLine } },
axisTick: { show: false },
axisLabel: {
color: PALETTE.ink,
fontSize: 14,
fontFamily: FONT,
margin: 14,
},
splitLine: { show: false },
};
/**
* 期间标签简写:2026H1 → 26H12026H2 → 26全年。
* CSV 里保留四位年份(无歧义、便于核对),只在轴上简化显示。
* 非 20xx 开头的标签(如"年初")原样返回。
*/
export function shortPeriod(label) {
return String(label)
.replace(/^20(\d{2})H2$/, '$1全年') // H2 = 全年累计
.replace(/^20(\d{2})/, '$1'); // 其余去掉世纪前缀
}
/** X 轴(期间):在 categoryAxis 基础上套用简写 */
export const periodAxis = {
...categoryAxis,
axisLabel: {
...categoryAxis.axisLabel,
formatter: shortPeriod,
},
};
export const titleStyle = {
left: 'center',
top: 18,
textStyle: {
color: PALETTE.ink,
fontSize: 19,
fontWeight: 'bold',
fontFamily: FONT,
},
subtextStyle: {
color: PALETTE.grey,
fontSize: 13,
fontFamily: FONT,
lineHeight: 20,
},
};
/**
* 标准布局常量
* 经多图表调试验证的标题-图例-绘图区间距,确保视觉呼吸感一致
*/
export const LAYOUT = {
titleTop: 18, // 标题顶部边距(已内置在 titleStyle 中)
legendTop: 85, // 图例顶部边距(副标题到图例约 25px)
gridTop: 145, // 绘图区顶部边距(图例到绘图区约 60px)
};
export const legendStyle = {
top: 62, // ⚠️ 已废弃:请改用 LAYOUT.legendTop 或 standardLegend()
itemGap: 26,
itemWidth: 18,
// ❌ 删除 icon: 'roundRect'
// 原因:它会覆盖 ECharts 按系列类型自动生成的图例样式(折线图的"短线+空心圆"
// 现在让每个图表按需设置,或使用 standardLegend() 获得正确默认值
textStyle: {
color: PALETTE.ink,
fontSize: 14,
fontFamily: FONT,
},
};
/** 折线标注:带白底描边,避免压在网格线上看不清 */
export function labelStyle(color, position = 'top') {
return {
show: true,
position,
color,
fontSize: 13,
fontFamily: FONT,
fontWeight: 500,
distance: 8,
};
}
/**
* 标准图例配置
* 不设置 icon,让 ECharts 按系列类型自动生成(折线→短线+空心圆,柱状→矩形)
* 统一使用 LAYOUT.legendTop 保证间距一致
*/
export function standardLegend(data, options = {}) {
return {
top: LAYOUT.legendTop,
itemGap: options.itemGap ?? 28,
textStyle: {
color: PALETTE.ink,
fontSize: 14,
fontFamily: FONT,
},
data,
...options, // 允许覆盖任何字段
};
}
/**
* 空心圆点折线系列 —— 标准配置
* 白心 + 彩色边框,视觉统一(symbolSize: 9, borderWidth: 2.5
*/
export function lineSeries({ name, data, color, labelPosition = 'top', formatter, width = 3 }) {
return {
name,
type: 'line',
data,
smooth: false,
symbol: 'circle',
symbolSize: 9,
itemStyle: {
color: '#fff',
borderColor: color,
borderWidth: 2.5,
},
lineStyle: { color, width },
emphasis: { focus: 'series' },
label: {
...labelStyle(color, labelPosition),
formatter,
},
};
}
/**
* 柱状图系列标准配置
* 强制显示所有标签(overflow: 'none'),避免小柱子标签被 ECharts 自动隐藏
*/
export function barSeries({ name, data, color, barWidth, formatter, ...rest }) {
return {
name,
type: 'bar',
data,
barWidth,
itemStyle: { color },
label: {
show: true,
position: 'top',
fontFamily: FONT,
fontSize: 11,
color: PALETTE.ink,
formatter: formatter ?? ((p) => p.value),
overflow: 'none', // 关键:强制显示所有标签
},
...rest,
};
}
/** 底部注释 */
export function footnote(text) {
return {
type: 'text',
left: 'center',
bottom: 12,
style: {
text,
fill: PALETTE.grey,
fontSize: 12,
fontFamily: FONT,
lineHeight: 18,
},
};
}
/**
* 平铺水印。ECharts 没有原生水印,用 graphic 数组铺满画布。
* 旋转 -20°,透明度压到 0.08,不干扰读数。
*/
export function watermarks(width, height, text = WATERMARK_TEXT) {
const items = [];
const stepX = 300;
const stepY = 200;
for (let x = -100; x < width + stepX; x += stepX) {
for (let y = 40; y < height + stepY; y += stepY) {
const offset = (Math.floor(y / stepY) % 2) * (stepX / 2);
items.push({
type: 'text',
left: x + offset,
top: y,
silent: true,
rotation: 0.35,
style: {
text,
fill: 'rgba(120, 100, 80, 0.10)',
fontSize: 20,
fontWeight: 'bold',
fontFamily: FONT,
},
});
}
}
return items;
}