导读:本期聚焦于小伙伴创作的《如何用JavaScript原生Canvas API绘制柱状图?完整教程与代码示例》,敬请观看详情,探索知识的价值。以下视频、文章将为您系统阐述其核心内容与价值。如果您觉得《如何用JavaScript原生Canvas API绘制柱状图?完整教程与代码示例》有用,将其分享出去将是对创作者最好的鼓励。

怎样用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方法会适配设备像素比,避免高清屏下图表模糊
  • 坐标轴绘制包含刻度、标签和网格线,数据展示更清晰
  • 柱状条使用圆角矩形绘制,视觉效果更友好,同时顶部显示对应数值
  • 鼠标悬停时柱状条会切换为高亮色,并且鼠标指针变为手型,提升交互体验

扩展优化方向

如果需要更完善的柱状图功能,可以基于上述代码进行扩展:

  • 添加图例说明,对应不同数据系列的颜色含义
  • 支持多组数据对比,每组数据用不同颜色柱状条并列展示
  • 添加动画效果,柱状条绘制时带有从底部向上生长的过渡动画
  • 支持点击柱状条触发自定义事件,实现下钻查看详情等功能
  • 增加响应式适配,窗口尺寸变化时自动重新计算布局并重绘图表

JavaScript柱状图Canvas绘图数据可视化原生JS图表前端绘图

免责声明:已尽一切努力确保本网站所含信息的准确性。网站部分内容来源于网络或由用户自行发表,内容观点不代表本站立场。本站是个人网站免费分享,内容仅供个人学习、研究或参考使用,如内容中引用了第三方作品,其版权归原作者所有。若内容触犯了您的权益,请联系我们进行处理。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。前端、网络、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握网站开发与运维所需的核心技术栈。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端逻辑,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。