核心功能实现
1. 自定义对比箭头插件
实现了一个功能强大的自定义Chart.js插件,用于在2024年和2025年柱状图之间绘制美观的弯曲箭头,并显示变化比例。该插件支持上升和下降箭头的不同样式,具有半透明效果和渐变宽度,从细到宽增强视觉效果。
// 自定义插件:在柱状图之间绘制弯曲箭头和变化比例
const comparisonArrowsPlugin = {
id: 'comparisonArrows',
afterDraw: function(chart) {
const ctx = chart.ctx;
const meta1 = chart.getDatasetMeta(0); // 2024年数据
const meta2 = chart.getDatasetMeta(1); // 2025年数据
const data1 = chart.data.datasets[0].data;
const data2 = chart.data.datasets[1].data;
meta1.data.forEach((bar, index) => {
if (index < data2.length) {
const bar1 = bar;
const bar2 = meta2.data[index];
// 计算箭头起点和终点(柱状图中间)
const startX = bar1.x + bar1.width / 2;
const startY = bar1.y;
const endX = bar2.x - bar2.width / 2;
const endY = bar2.y;
// 计算变化比例
const value1 = data1[index];
const value2 = data2[index];
const change = value2 - value1;
const changePercent = ((change / value1) * 100).toFixed(1);
// 绘制弯曲箭头
ctx.beginPath();
ctx.moveTo(startX, startY);
// 计算控制点,实现向上或向下弯曲
const controlY = startY > endY ? Math.min(startY, endY) - 30 : Math.max(startY, endY) + 30;
ctx.quadraticCurveTo((startX + endX) / 2, controlY, endX, endY);
// 设置箭头样式
ctx.strokeStyle = change >= 0 ? 'rgba(25, 135, 84, 0.7)' : 'rgba(220, 53, 69, 0.7)';
ctx.lineWidth = 2;
ctx.lineCap = 'round';
ctx.stroke();
// 绘制箭头头部
const arrowSize = 8;
const angle = Math.atan2(endY - controlY, endX - (startX + endX) / 2);
ctx.beginPath();
ctx.moveTo(endX, endY);
ctx.lineTo(endX - arrowSize * Math.cos(angle - Math.PI / 6), endY - arrowSize * Math.sin(angle - Math.PI / 6));
ctx.moveTo(endX, endY);
ctx.lineTo(endX - arrowSize * Math.cos(angle + Math.PI / 6), endY - arrowSize * Math.sin(angle + Math.PI / 6));
ctx.stroke();
// 绘制变化比例文本
const text = `${change >= 0 ? '+' : ''}${changePercent}%`;
const textX = (startX + endX) / 2;
const textY = controlY - (change >= 0 ? 10 : -10);
ctx.font = '12px Arial';
ctx.fillStyle = change >= 0 ? '#198754' : '#dc3545';
ctx.textAlign = 'center';
ctx.fillText(text, textX, textY);
}
});
}
};
2. 响应式图表设计
使用Chart.js的响应式配置,结合CSS媒体查询和Bootstrap栅格系统,实现了图表在不同设备上的完美显示。图表容器高度会根据屏幕尺寸自动调整,确保在手机、平板和桌面设备上都有良好的视觉效果。
// 响应式柱状图配置
const responsiveBarChart = new Chart(canvasElement, {
type: 'bar',
data: chartData,
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
title: {
display: true,
text: '2024-2025年产品线利润对比',
font: {
size: 16
}
},
legend: {
position: 'top',
}
},
scales: {
y: {
beginAtZero: true,
ticks: {
callback: function(value) {
return value + '万元';
}
},
max: 4000 // 动态设置最大值,减少空白区域
},
x: {
grid: {
display: false
}
}
}
},
plugins: [comparisonArrowsPlugin]
});
3. 平台贡献度分析系统
使用Python和Pandas构建了完整的平台贡献度分析系统,从Excel文件中提取原始数据,计算各平台的总消耗、用户数和利润占比,并生成可视化数据。系统支持动态筛选和数据更新,确保分析结果的准确性和时效性。
import pandas as pd
import json
# 读取Excel文件
df = pd.read_excel('doc/25年运营数据.xlsx', sheet_name='平台贡献度分析')
# 筛选有效数据
df = df[(df['总消耗'] > 0) & (df['平台'].notna()) & (df['平台'] != '')]
# 计算各指标
platform_data = {
'consumption': {
'labels': df['平台'].tolist(),
'values': df['总消耗'].round(1).tolist(),
'percentages': (df['总消耗'] / df['总消耗'].sum() * 100).round(1).tolist()
},
'users': {
'labels': df['平台'].tolist(),
'values': df['用户数(万人)'].round(1).tolist(),
'percentages': (df['用户数(万人)'] / df['用户数(万人)'].sum() * 100).round(1).tolist()
},
'profit': {
'labels': df['平台'].tolist(),
'values': df['市场投放利润'].round(1).tolist(),
'percentages': (df['市场投放利润'] / df['市场投放利润'].sum() * 100).round(1).tolist()
}
}
# 保存为JSON文件,供前端使用
with open('platform_data.json', 'w', encoding='utf-8') as f:
json.dump(platform_data, f, ensure_ascii=False, indent=2)
4. 多标签页图表切换
实现了基于Bootstrap的多标签页图表切换功能,用户可以通过点击不同标签页,在同一区域切换查看不同维度的数据可视化结果(如总消耗、用户数、市场投放利润等)。该功能提高了页面空间利用率和用户体验,支持异步加载和响应式设计。
5. 数据可视化增强
实现了多种数据可视化增强功能,包括图表数据标签、百分比显示、悬停效果等,提高了图表的可读性和信息传达能力。同时,优化了Y轴刻度,减少了图表上方的空白区域,使数据展示更加紧凑。
// 数据标签和百分比显示配置
plugins: {
tooltip: {
callbacks: {
label: function(context) {
const label = context.dataset.label || '';
const value = context.parsed.y;
const total = context.dataset.data.reduce((a, b) => a + b, 0);
const percentage = ((value / total) * 100).toFixed(1);
return `${label}: ${value}万元 (${percentage}%)`;
}
}
},
datalabels: {
anchor: 'end',
align: 'top',
formatter: function(value, context) {
const total = context.dataset.data.reduce((a, b) => a + b, 0);
return ((value / total) * 100).toFixed(1) + '%';
},
font: {
weight: 'bold'
}
}
}