目录
整体架构
代码使用纯 JavaScript 和 Canvas API 实现了五种基本图表,采用模块化设计:
- 图表数据统一存储在
data
对象中 - 使用
ChartUtils
工具类封装通用方法 - 每种图表有独立的绘制函数
- 添加了响应式处理,支持窗口大小变化时重绘
数据结构设计
const data = {
labels: ['一月', '二月', '三月', '四月', '五月', '六月'],
values: [65, 59, 80, 81, 56, 55],
colors: ['#3B82F6', '#6366F1', '#8B5CF6', '#A855F7', '#C026D3', '#DB2777'],
scatterData: Array.from({length: 20}, () => ({
x: Math.random() * 300,
y: Math.random() * 200,
r: Math.random() * 10 + 5
}))
};
labels
和values
用于大多数图表colors
定义了图表的配色方案scatterData
为散点图生成随机数据点
工具函数
const ChartUtils = {
getColor: (index) => data.colors[index % data.colors.length],
drawLabel: (ctx, text, x, y, align = 'center') => {
ctx.save();
ctx.font = '12px Inter';
ctx.textAlign = align;
ctx.textBaseline = 'middle';
ctx.fillText(text, x, y);
ctx.restore();
}
};
getColor
:循环使用预设颜色drawLabel
:封装文本绘制逻辑,保存 / 恢复上下文状态避免样式冲突
图表实现细节
1. 条形图(Bar Chart)
function drawBarChart() {
// 计算边距和坐标系
const margin = {top: 20, right: 20, bottom: 40, left: 40};
const width = barChart.width - margin.left - margin.right;
const height = barChart.height - margin.top - margin.bottom;
const barWidth = width / data.labels.length * 0.7;
const maxValue = Math.max(...data.values) * 1.1;
// 绘制坐标轴
barCtx.beginPath();
barCtx.moveTo(margin.left, margin.top);
barCtx.lineTo(margin.left, margin.top + height);
barCtx.lineTo(margin.left + width, margin.top + height);
barCtx.strokeStyle = '#ccc';
barCtx.stroke();
// 绘制条形和标签
data.values.forEach((value, index) => {
// 计算条形位置和高度
const barHeight = (value / maxValue) * height;
const x = margin.left + (width / data.labels.length) * index + (width / data.labels.length - barWidth) / 2;
const y = margin.top + height - barHeight;
// 绘制条形
barCtx.fillStyle = ChartUtils.getColor(index);
barCtx.fillRect(x, y, barWidth, barHeight);
// 绘制标签
ChartUtils.drawLabel(barCtx, value, x + barWidth / 2, y - 10);
ChartUtils.drawLabel(barCtx, data.labels[index], x + barWidth / 2, margin.top + height + 20);
});
}
- 关键计算:根据最大值缩放高度,使用边距留出标签空间
- 坐标轴绘制:使用 Canvas 路径绘制简单的 XY 轴
- 数据可视化:通过矩形高度表示数值大小
代码效果展示:
2. 饼图(Pie Chart)
function drawPieChart() {
// 计算中心点和半径
const centerX = pieChart.width / 2;
const centerY = pieChart.height / 2;
const radius = Math.min(centerX, centerY) * 0.7;
const total = data.values.reduce((a, b) => a + b, 0);
let startAngle = 0;
// 绘制扇形
data.values.forEach((value, index) => {
const sliceAngle = (value / total) * Math.PI * 2;
const endAngle = startAngle + sliceAngle;
// 绘制扇形
pieCtx.beginPath();
pieCtx.moveTo(centerX, centerY);
pieCtx.arc(centerX, centerY, radius, startAngle, endAngle);
pieCtx.closePath();
pieCtx.fillStyle = ChartUtils.getColor(index);
pieCtx.fill();
// 绘制标签和连接线
const labelAngle = startAngle + sliceAngle / 2;
const labelX = centerX + Math.cos(labelAngle) * (radius * 1.2);
const labelY = centerY + Math.sin(labelAngle) * (radius * 1.2);
// 绘制连接线
pieCtx.beginPath();
pieCtx.moveTo(centerX + Math.cos(labelAngle) * radius, centerY + Math.sin(labelAngle) * radius);
pieCtx.lineTo(labelX, labelY);
pieCtx.strokeStyle = '#ccc';
pieCtx.stroke();
// 绘制标签文本
ChartUtils.drawLabel(
pieCtx,
`${data.labels[index]} (${((value / total) * 100).toFixed(1)}%)`,
labelX + (labelAngle > Math.PI/2 && labelAngle < Math.PI*3/2 ? -10 : 10),
labelY,
labelAngle > Math.PI/2 && labelAngle < Math.PI*3/2 ? 'right' : 'left'
);
startAngle = endAngle;
});
}
- 关键计算:使用弧度计算扇形角度(
2π
表示完整圆) - 标签优化:根据扇形位置智能调整标签对齐方式
- 百分比计算:自动计算并显示每个扇形占总体的百分比
代码效果展示:
3. 玫瑰图(Rose Chart)
function drawRoseChart() {
const centerX = roseChart.width / 2;
const centerY = roseChart.height / 2;
const maxRadius = Math.min(centerX, centerY) * 0.7;
const maxValue = Math.max(...data.values);
const angleStep = (Math.PI * 2) / data.labels.length;
// 绘制扇形
data.values.forEach((value, index) => {
const angle = index * angleStep;
const radius = (value / maxValue) * maxRadius;
// 绘制扇形
roseCtx.beginPath();
roseCtx.moveTo(centerX, centerY);
roseCtx.arc(centerX, centerY, radius, angle, angle + angleStep);
roseCtx.closePath();
roseCtx.fillStyle = ChartUtils.getColor(index);
roseCtx.fill();
roseCtx.strokeStyle = '#fff';
roseCtx.lineWidth = 1;
roseCtx.stroke();
// 绘制标签
const labelAngle = angle + angleStep / 2;
const labelX = centerX + Math.cos(labelAngle) * (radius + 20);
const labelY = centerY + Math.sin(labelAngle) * (radius + 20);
ChartUtils.drawLabel(pieCtx, data.labels[index], labelX, labelY);
});
}
- 与饼图的区别:玫瑰图使用半径而非角度来表示数值大小
- 角度计算:平均分配每个类别对应的角度
- 视觉效果:通过不同半径的扇形形成花瓣状分布
代码效果展示:
4. 折线图(Line Chart)
function drawLineChart() {
const margin = {top: 20, right: 20, bottom: 40, left: 40};
const width = lineChart.width - margin.left - margin.right;
const height = lineChart.height - margin.top - margin.bottom;
const maxValue = Math.max(...data.values) * 1.1;
// 绘制坐标轴
lineCtx.beginPath();
lineCtx.moveTo(margin.left, margin.top);
lineCtx.lineTo(margin.left, margin.top + height);
lineCtx.lineTo(margin.left + width, margin.top + height);
lineCtx.strokeStyle = '#ccc';
lineCtx.stroke();
// 计算点的位置
const points = data.values.map((value, index) => ({
x: margin.left + (width / (data.labels.length - 1)) * index,
y: margin.top + height - (value / maxValue) * height,
value
}));
// 绘制折线
lineCtx.beginPath();
lineCtx.moveTo(points[0].x, points[0].y);
points.forEach(point => {
lineCtx.lineTo(point.x, point.y);
});
lineCtx.strokeStyle = '#3B82F6';
lineCtx.lineWidth = 2;
lineCtx.stroke();
// 绘制数据点和标签
points.forEach(point => {
lineCtx.beginPath();
lineCtx.arc(point.x, point.y, 5, 0, Math.PI * 2);
lineCtx.fillStyle = '#3B82F6';
lineCtx.fill();
lineCtx.strokeStyle = '#fff';
lineCtx.lineWidth = 1;
lineCtx.stroke();
// 绘制值标签和类别标签
ChartUtils.drawLabel(lineCtx, point.value, point.x, point.y - 15);
ChartUtils.drawLabel(lineCtx, data.labels[points.indexOf(point)], point.x, margin.top + height + 20);
});
}
- 点位置计算:根据数据值和最大值得出 Y 坐标,根据索引计算 X 坐标
- 折线绘制:使用 Canvas 路径连接所有点
- 视觉增强:在数据点周围添加白色边框提高可读性
代码效果展示:
5. 散点图(Scatter Chart)
function drawScatterChart() {
const margin = {top: 20, right: 20, bottom: 20, left: 20};
const width = scatterChart.width - margin.left - margin.right;
const height = scatterChart.height - margin.top - margin.bottom;
// 绘制坐标轴
scatterCtx.beginPath();
scatterCtx.moveTo(margin.left, margin.top);
scatterCtx.lineTo(margin.left, margin.top + height);
scatterCtx.lineTo(margin.left + width, margin.top + height);
scatterCtx.strokeStyle = '#ccc';
scatterCtx.stroke();
// 绘制散点
data.scatterData.forEach((point, index) => {
scatterCtx.beginPath();
scatterCtx.arc(
margin.left + point.x,
margin.top + height - point.y,
point.r,
0,
Math.PI * 2
);
scatterCtx.fillStyle = ChartUtils.getColor(index);
scatterCtx.fill();
scatterCtx.strokeStyle = '#fff';
scatterCtx.lineWidth = 1;
scatterCtx.stroke();
});
}
- 数据结构:使用包含 x、y 坐标和半径的对象数组
- 坐标映射:直接使用数据中的坐标值,减去边距进行偏移
- 视觉变化:随机大小的点增加视觉丰富度
代码效果展示:
响应式处理
window.addEventListener('resize', () => {
// 重置每个Canvas的尺寸并重绘
barChart.width = barChart.offsetWidth;
barChart.height = barChart.offsetHeight;
drawBarChart();
// 其他图表类似...
});
- 监听窗口大小变化事件
- 重置 Canvas 尺寸(重要:重置尺寸会清除画布内容)
- 重新调用绘制函数
样式设计
- 使用 Tailwind CSS 定义图表容器样式
- 采用现代蓝色系配色方案
- 添加悬停效果增强交互感
- 合理使用阴影和圆角提升视觉层次
这个图表库实现了基本的数据可视化功能,通过模块化设计和清晰的代码结构,便于后续扩展更多图表类型或添加交互功能。