怎样用JavaScript创建柱状图
柱状图是数据可视化中最常用的图表类型之一,能够直观展示不同类别数据的大小关系。原生JavaScript结合Canvas API就可以实现基础的柱状图绘制,不需要依赖第三方图表库,适合轻量场景使用。下面我们通过一个完整示例,讲解使用JavaScript创建柱状图的核心步骤。
核心实现思路
使用原生JavaScript绘制柱状图主要分为以下几个步骤:
- 准备Canvas画布,获取2D绘图上下文
- 定义柱状图的基础配置,包括画布尺寸、边距、颜色、数据等
- 计算坐标轴和柱状图的布局参数,确保图表内容适配画布
- 绘制坐标轴、刻度、刻度标签等基础元素
- 根据数据绘制每一根柱状条,添加对应的数值标签
- 可选添加鼠标交互效果,提升用户体验
完整实现代码
下面的代码实现了一个可复用的基础柱状图,支持自定义数据、颜色和尺寸,同时添加了鼠标悬停高亮的交互效果。
// 柱状图构造函数
function BarChart(options) {
this.canvas = options.canvas;
this.ctx = this.canvas.getContext('2d');
this.data = options.data || []; // 格式:[{label: '类别1', value: 100}, ...]
this.colors = options.colors || ['#5470c6', '#91cc75', '#fac858', '#ee6666', '#73c0de'];
this.margin = options.margin || { top: 40, right: 40, bottom: 60, left: 60 };
this.barPadding = options.barPadding || 0.3; // 柱状条间距比例
this.title = options.title || '柱状图';
this.hoverColor = options.hoverColor || '#ff9f43';
this.hoveredIndex = -1;
// 初始化画布尺寸
this.initCanvasSize();
// 绑定鼠标事件
this.bindEvents();
// 绘制图表
this.draw();
}
// 初始化画布尺寸,适配高清屏
BarChart.prototype.initCanvasSize = function() {
const dpr = window.devicePixelRatio || 1;
const rect = this.canvas.getBoundingClientRect();
this.canvas.width = rect.width * dpr;
this.canvas.height = rect.height * dpr;
this.ctx.scale(dpr, dpr);
this.canvas.style.width = rect.width + 'px';
this.canvas.style.height = rect.height + 'px';
// 计算绘图区域尺寸
this.plotWidth = rect.width - this.margin.left - this.margin.right;
this.plotHeight = rect.height - this.margin.top - this.margin.bottom;
};
// 绘制整个图表
BarChart.prototype.draw = function() {
// 清空画布
this.ctx.clearRect(0, 0, this.canvas.width / (window.devicePixelRatio || 1), this.canvas.height / (window.devicePixelRatio || 1));
// 绘制标题
this.drawTitle();
// 绘制坐标轴
this.drawAxes();
// 绘制柱状条
this.drawBars();
};
// 绘制标题
BarChart.prototype.drawTitle = function() {
this.ctx.font = 'bold 16px Arial';
this.ctx.fillStyle = '#333';
this.ctx.textAlign = 'center';
this.ctx.fillText(this.title, this.canvas.width / (window.devicePixelRatio || 1) / 2, 25);
};
// 绘制坐标轴
BarChart.prototype.drawAxes = function() {
const originX = this.margin.left;
const originY = this.margin.top + this.plotHeight;
// 绘制Y轴
this.ctx.beginPath();
this.ctx.moveTo(originX, this.margin.top);
this.ctx.lineTo(originX, originY);
this.ctx.strokeStyle = '#666';
this.ctx.lineWidth = 1;
this.ctx.stroke();
// 绘制X轴
this.ctx.beginPath();
this.ctx.moveTo(originX, originY);
this.ctx.lineTo(originX + this.plotWidth, originY);
this.ctx.stroke();
// 绘制Y轴刻度和标签
this.drawYAxisTicks(originX, originY);
// 绘制X轴刻度和标签
this.drawXAxisTicks(originX, originY);
};
// 绘制Y轴刻度和标签
BarChart.prototype.drawYAxisTicks = function(originX, originY) {
if (this.data.length === 0) return;
// 计算最大值,向上取整方便刻度显示
const maxValue = Math.max(...this.data.map(item => item.value));
const yMax = Math.ceil(maxValue / 10) * 10;
const tickCount = 5; // 刻度数量
const tickStep = yMax / tickCount;
const tickLength = 5;
this.ctx.font = '12px Arial';
this.ctx.fillStyle = '#666';
this.ctx.textAlign = 'right';
this.ctx.textBaseline = 'middle';
for (let i = 0; i <= tickCount; i++) {
const value = tickStep * i;
const y = originY - (value / yMax) * this.plotHeight;
// 绘制刻度线
this.ctx.beginPath();
this.ctx.moveTo(originX, y);
this.ctx.lineTo(originX - tickLength, y);
this.ctx.stroke();
// 绘制刻度标签
this.ctx.fillText(value, originX - tickLength - 5, y);
// 绘制横向网格线(除了最底部的Y轴基线)
if (i > 0) {
this.ctx.beginPath();
this.ctx.moveTo(originX, y);
this.ctx.lineTo(originX + this.plotWidth, y);
this.ctx.strokeStyle = '#eee';
this.ctx.lineWidth = 0.5;
this.ctx.stroke();
// 恢复轴线样式
this.ctx.strokeStyle = '#666';
this.ctx.lineWidth = 1;
}
}
};
// 绘制X轴刻度和标签
BarChart.prototype.drawXAxisTicks = function(originX, originY) {
if (this.data.length === 0) return;
const barAreaWidth = this.plotWidth;
const barTotalWidth = barAreaWidth / this.data.length;
const barWidth = barTotalWidth * (1 - this.barPadding);
const barOffset = barTotalWidth * this.barPadding / 2;
this.ctx.font = '12px Arial';
this.ctx.fillStyle = '#666';
this.ctx.textAlign = 'center';
this.ctx.textBaseline = 'top';
this.data.forEach((item, index) => {
const x = originX + barTotalWidth * index + barOffset + barWidth / 2;
// 绘制刻度线
this.ctx.beginPath();
this.ctx.moveTo(x, originY);
this.ctx.lineTo(x, originY + 5);
this.ctx.stroke();
// 绘制标签
this.ctx.fillText(item.label, x, originY + 10);
});
};
// 绘制柱状条
BarChart.prototype.drawBars = function() {
if (this.data.length === 0) return;
const originX = this.margin.left;
const originY = this.margin.top + this.plotHeight;
const maxValue = Math.max(...this.data.map(item => item.value));
const yMax = Math.ceil(maxValue / 10) * 10;
const barAreaWidth = this.plotWidth;
const barTotalWidth = barAreaWidth / this.data.length;
const barWidth = barTotalWidth * (1 - this.barPadding);
const barOffset = barTotalWidth * this.barPadding / 2;
this.data.forEach((item, index) => {
const barHeight = (item.value / yMax) * this.plotHeight;
const x = originX + barTotalWidth * index + barOffset;
const y = originY - barHeight;
// 选择颜色,悬停时切换为高亮色
const color = index === this.hoveredIndex ? this.hoverColor : this.colors[index % this.colors.length];
this.ctx.fillStyle = color;
// 绘制圆角柱状条
this.drawRoundRect(x, y, barWidth, barHeight, 4);
this.ctx.fill();
// 绘制数值标签
this.ctx.font = '12px Arial';
this.ctx.fillStyle = '#333';
this.ctx.textAlign = 'center';
this.ctx.textBaseline = 'bottom';
this.ctx.fillText(item.value, x + barWidth / 2, y - 5);
});
};
// 绘制圆角矩形
BarChart.prototype.drawRoundRect = function(x, y, width, height, radius) {
if (height === 0) return;
this.ctx.beginPath();
this.ctx.moveTo(x + radius, y);
this.ctx.lineTo(x + width - radius, y);
this.ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
this.ctx.lineTo(x + width, y + height);
this.ctx.lineTo(x, y + height);
this.ctx.lineTo(x, y + radius);
this.ctx.quadraticCurveTo(x, y, x + radius, y);
this.ctx.closePath();
};
// 绑定鼠标事件
BarChart.prototype.bindEvents = function() {
const self = this;
this.canvas.addEventListener('mousemove', function(e) {
const rect = self.canvas.getBoundingClientRect();
const mouseX = e.clientX - rect.left;
const mouseY = e.clientY - rect.top;
const newHoveredIndex = self.getBarIndexAt(mouseX, mouseY);
if (newHoveredIndex !== self.hoveredIndex) {
self.hoveredIndex = newHoveredIndex;
self.draw(); // 重新绘制更新高亮状态
// 修改鼠标样式
self.canvas.style.cursor = newHoveredIndex >= 0 ? 'pointer' : 'default';
}
});
this.canvas.addEventListener('mouseleave', function() {
if (self.hoveredIndex >= 0) {
self.hoveredIndex = -1;
self.draw();
self.canvas.style.cursor = 'default';
}
});
};
// 获取鼠标位置对应的柱状条索引
BarChart.prototype.getBarIndexAt = function(x, y) {
if (this.data.length === 0) return -1;
const originX = this.margin.left;
const originY = this.margin.top + this.plotHeight;
const barAreaWidth = this.plotWidth;
const barTotalWidth = barAreaWidth / this.data.length;
const barWidth = barTotalWidth * (1 - this.barPadding);
const barOffset = barTotalWidth * this.barPadding / 2;
for (let i = 0; i < this.data.length; i++) {
const barX = originX + barTotalWidth * i + barOffset;
// 判断鼠标是否在当前柱状条的x范围内,且在绘图区域内
if (x >= barX && x <= barX + barWidth && y >= this.margin.top && y <= originY) {
return i;
}
}
return -1;
}
// 使用示例
window.onload = function() {
const canvas = document.getElementById('barChart');
// 设置画布初始尺寸
canvas.style.width = '800px';
canvas.style.height = '500px';
const chartData = [
{ label: '一月', value: 320 },
{ label: '二月', value: 280 },
{ label: '三月', value: 410 },
{ label: '四月', value: 350 },
{ label: '五月', value: 480 },
{ label: '六月', value: 520 }
];
new BarChart({
canvas: canvas,
data: chartData,
title: '上半年月度销售额(单位:万元)'
});
};HTML页面结构
需要在HTML页面中添加Canvas元素来承载柱状图,页面完整代码如下:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>JavaScript柱状图示例</title>
<style>
body {
font-family: Arial, sans-serif;
display: flex;
justify-content: center;
padding: 20px;
background-color: #f5f5f5;
}
.chart-container {
background-color: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
canvas {
display: block;
}
</style>
</head>
<body>
<div class="chart-container">
<canvas id="barChart">您的浏览器不支持Canvas,请升级浏览器查看柱状图</canvas>
</div>
<script src="bar-chart.js"></script>
</body>
</html>代码功能说明
上述实现将柱状图的绘制逻辑封装为BarChart构造函数,通过传入配置参数即可快速生成图表:
- 构造函数支持自定义画布、数据、颜色、边距、标题等配置,复用性较强
initCanvasSize方法会适配设备像素比,避免高清屏下图表模糊- 坐标轴绘制包含刻度、标签和网格线,数据展示更清晰
- 柱状条使用圆角矩形绘制,视觉效果更友好,同时顶部显示对应数值
- 鼠标悬停时柱状条会切换为高亮色,并且鼠标指针变为手型,提升交互体验
扩展优化方向
如果需要更完善的柱状图功能,可以基于上述代码进行扩展:
- 添加图例说明,对应不同数据系列的颜色含义
- 支持多组数据对比,每组数据用不同颜色柱状条并列展示
- 添加动画效果,柱状条绘制时带有从底部向上生长的过渡动画
- 支持点击柱状条触发自定义事件,实现下钻查看详情等功能
- 增加响应式适配,窗口尺寸变化时自动重新计算布局并重绘图表