在之前的教程中,我们介绍了如何使用HTML5画布绘制饼状图或甜甜圈图。在本教程中,我将向您展示如何使用JavaScript和HTML5画布,通过使用条形图以图形方式显示数据。
什么是柱状图?
柱状图是用于表示数字数据的非常常见的工具。从财务报告到PowerPoint演示文稿再到信息图表,柱状图都被经常使用,因为它们提供了一种非常容易理解的数字数据视图。
条形图使用条形来表示数字数据,条形是矩形,其宽度或高度与它们所代表的数字数据成正比。
条形图有许多类型,例如:
- 水平条形图和垂直条形图,取决于图表的方向
- 堆叠式条形图或经典的条形图,用于表示多个系列的数据
- 二维或三维条形图
条形图的组成部分是什么?
让我们来看看构成柱状图的组成部分,无论其类型如何:

- 图表数据:这些是一组数字和相关的类别,由图表来表示。
- 数据系列的名称(1)。
- 图表网格(2):提供一个参考系统,使视觉表现易于理解。
- 条形图(3):填充颜色的矩形,其尺寸与所代表的数据成比例。
- 图表图例(4):显示所使用的颜色和它们所代表的数据之间的对应关系。
现在我们知道了条形图的组成部分,让我们看看如何编写JavaScript代码来绘制这样的图表。
使用JavaScript绘制柱状图
设置JS项目
要开始使用JavaScript和HTML5画布绘制,我们需要这样设置我们的项目:
- 创建一个文件夹来存放项目文件;让我们把这个文件夹称为
bar-chart-tutorial。 - 在项目文件夹中,创建一个文件,并将其称为index.html。这将包含我们的HTML代码。
- 同样在项目文件夹中,创建一个文件,并将其称为script.js。这将包含用于绘制柱状图的JavaScript代码。
我们将保持事情非常简单,在index.html中添加以下代码:
<html>
<body>
<canvas id="myCanvas"></canvas>
<script type="text/javascript" src="script.js"></script>
</body>
</html>
myCanvas 我们有一个ID为<canvas> 的元素,这样我们就可以在我们的JS代码中引用它。然后我们通过<script> 标签加载JS代码。
在script.js文件中添加以下代码:
var myCanvas = document.getElementById("myCanvas");
myCanvas.width = 500;
myCanvas.height = 500;
var ctx = myCanvas.getContext("2d");
这将获得对canvas元素的引用,然后将宽度和高度设置为300px 。要在canvas上绘图,我们只需要对其2D上下文的引用,其中包含所有的绘图方法。
添加一些辅助函数
绘制柱状图只需要知道如何绘制两个元素:
- 绘制一条线:用于绘制网格线
- 绘制一个充满颜色的矩形:用于绘制图表中的条形。
让我们为这两个元素创建辅助JS函数。我们将在我们的script.js文件中添加这些函数:
function drawLine(ctx, startX, startY, endX, endY,color){
ctx.save();
ctx.strokeStyle = color;
ctx.beginPath();
ctx.moveTo(startX,startY);
ctx.lineTo(endX,endY);
ctx.stroke();
ctx.restore();
}
drawLine 这个函数需要六个参数:
ctx:对绘图环境的引用startX: 线条起点的X坐标startY线条起点的Y坐标endX: 线条终点的X坐标endY线条终点的Y坐标color: 线条的颜色
我们正在修改strokeStyle 的颜色设置。这决定了用于绘制线条的颜色。我们使用ctx.save() 和ctx.restore() ,这样我们就不会影响到这个函数之外的颜色使用。
我们通过调用beginPath() 来画线。这通知绘图上下文,我们开始在画布上绘制新的东西。我们使用moveTo() 来设置起点,调用lineTo() 来指示终点,然后通过调用stroke() 来进行实际的绘制。
我们需要的另一个辅助函数是一个绘制条形的函数--它是一个充满色彩的矩形。让我们把它添加到script.js中:
function drawBar(ctx, upperLeftCornerX, upperLeftCornerY, width, height,color){
ctx.save();
ctx.fillStyle=color;
ctx.fillRect(upperLeftCornerX,upperLeftCornerY,width,height);
ctx.restore();
}
drawBar 这个函数需要六个参数:
ctx:对绘图环境的引用upperLeftCornerX: 条形的左上角的X坐标upperLeftCornerY: 条形图左上角的X坐标width条形图的宽度height条形图的高度color条形图的颜色
条形图的数据模型
现在我们已经有了辅助函数,让我们继续来看看图表的数据模型。所有类型的图表,包括条形图,背后都有一个数据模型。数据模型是一个结构化的数字数据集。在本教程中,我们将使用一个由类别及其相关数值组成的数据系列,代表我收藏的黑胶唱片的数量,按音乐流派分组:
- 古典音乐:16
- 另类摇滚:12
- 流行音乐:18
- 爵士乐:32
我们可以在JavaScript中以对象的形式来表示这一点。我们将把这个数据和其他信息一起传递给我们的BarChart 对象。让我们把它添加到我们的script.js文件中:
{
"Classical Music": 16,
"Alternative Rock": 12,
"Pop": 18,
"Jazz": 32,
}
实现BarChart类
我们的BarChart 类将有各种方法来帮助将代码分成独立的相关部分。条形图的不同组件将由不同的方法来绘制。
实现条形图组件
让我们来实现这个类和方法,它将对我们的条形图进行实际绘制。我们将通过在我们的script.js文件中添加以下JavaScript代码来实现:
class BarChart {
constructor(options) {
this.options = options;
this.canvas = options.canvas;
this.ctx = this.canvas.getContext("2d");
this.colors = options.colors;
this.titleOptions = options.titleOptions;
this.maxValue = Math.max(...Object.values(this.options.data));
}
drawGridLines() {
var canvasActualHeight = this.canvas.height - this.options.padding * 2;
var canvasActualWidth = this.canvas.width - this.options.padding * 2;
var gridValue = 0;
while (gridValue <= this.maxValue) {
var gridY =
canvasActualHeight * (1 - gridValue / this.maxValue) +
this.options.padding;
drawLine(
this.ctx,
0,
gridY,
this.canvas.width,
gridY,
this.options.gridColor
);
drawLine(
this.ctx,
15,
this.options.padding/2,
15,
gridY + this.options.padding/2,
this.options.gridColor
);
// Writing grid markers
this.ctx.save();
this.ctx.fillStyle = this.options.gridColor;
this.ctx.textBaseline = "bottom";
this.ctx.font = "bold 10px Arial";
this.ctx.fillText(gridValue, 0, gridY - 2);
this.ctx.restore();
gridValue += this.options.gridScale;
}
}
drawBars() {
var canvasActualHeight = this.canvas.height - this.options.padding * 2;
var canvasActualWidth = this.canvas.width - this.options.padding * 2;
var barIndex = 0;
var numberOfBars = Object.keys(this.options.data).length;
var barSize = canvasActualWidth / numberOfBars;
var values = Object.values(this.options.data);
for (let val of values) {
var barHeight = Math.round((canvasActualHeight * val) / this.maxValue);
console.log(barHeight);
drawBar(
this.ctx,
this.options.padding + barIndex * barSize,
this.canvas.height - barHeight - this.options.padding,
barSize,
barHeight,
this.colors[barIndex % this.colors.length]
);
barIndex++;
}
}
draw() {
this.drawGridLines();
this.drawBars();
}
}
该类开始定义构造方法,通过在不同的属性中存储作为参数传递的options ,进行一些基本的初始化。它存储了canvas 引用,并创建了一个同样作为类成员存储的绘图上下文。然后,它存储作为选项传递的colors 数组。它还从我们的数据对象中提取键的最大值并将其存储在maxValue 属性中。我们需要这个数字,因为我们将需要根据这个值和画布的大小来缩放所有的条形。否则,我们的条形图可能会超出显示区域,而我们不希望这样。
现在,我们将实现drawGridLines() 方法。这将绘制网格线和网格标记。
canvasActualHeight 和canvasActualWidth 变量存储了画布的高度和宽度,并使用通过options 传递的 padding 值进行调整。padding 变量表示画布的边缘和里面的图表之间的像素数。
然后我们绘制图表的网格线。options.gridStep 属性设置了用于绘制线条的步骤。因此,options.gridStep 为10意味着每10个单位绘制网格线。
为了绘制网格线,我们使用辅助函数drawLine() ;至于网格线的颜色,我们从options.gridColor 变量中获取。请注意,画布坐标从左上角的0,0 开始,向右和底部增加,而我们的网格值从底部向顶部增加。这就是为什么我们在计算gridY 值的公式中使用1 - gridValue/maxValue 。
对于每条网格线,我们也要在网格线上方2个像素处画出网格线的值(这就是为什么我们要用gridY - 2 来表示文字的Y坐标)。
接下来,我们通过使用drawBars() 方法来绘制条形图。计算每个条形图的高度和宽度的数学方法是非常简单的;它考虑到了填充和图表数据模型中每个类别的值和颜色。
使用条形图组件
现在让我们看看如何使用上面实现的BarChart 类。我们需要实例化该类并调用draw() 方法。在script.js文件中添加以下代码:
var myBarchart = new BarChart({
canvas: myCanvas,
padding: 40,
gridScale: 5,
gridColor: "black",
data: {
"Classical Music": 16,
"Alternative Rock": 12,
"Pop": 18,
"Jazz": 32,
},
colors: ["#a55ca5", "#67b6c7", "#bccd7a", "#eb9743"],
});
myBarchart.draw();
这段代码创建了一个带有所需选项的BarChart 类的新实例。我们将在本教程的下一节中编写使用titleOptions 对象的方法。在浏览器中加载index.html应该产生这样的结果:

添加数据系列名称和图表图例
为了在图表下面添加数据系列名称,我们需要在我们的BarChart 类中添加以下方法:
drawLabel() {
this.ctx.save();
this.ctx.textBaseline = "bottom";
this.ctx.textAlign = this.titleOptions.align;
this.ctx.fillStyle = this.titleOptions.fill;
this.ctx.font = `${this.titleOptions.font.weight} ${this.titleOptions.font.size} ${this.titleOptions.font.family}`;
let xPos = this.canvas.width / 2;
if (this.titleOptions.align == "left") {
xPos = 10;
}
if (this.titleOptions.align == "right") {
xPos = this.canvas.width - 10;
}
this.ctx.fillText(this.options.seriesName, xPos, this.canvas.height);
this.ctx.restore();
}
在我们的titleOptions 对象中,我们接受了一系列的字体属性,以设置诸如字体大小、字体家族和字体重量。我们还接受一个字符串值来决定图表标题的对齐方式。我们用这个值既可以控制标题的对齐方式,又可以确定新的字符串除了对齐方式之外的位置。
同时更新该类的draw() 方法,增加对drawLabel() 的调用:
draw() {
this.drawGridLines();
this.drawBars();
this.drawLabel();
}
我们还需要像这样改变实例化Barchart 类的方式:
var myBarchart = new Barchart(
{
canvas:myCanvas,
seriesName:"Vinyl records",
padding:20,
gridScale:5,
gridColor:"#eeeeee",
data: {
"Classical Music": 16,
"Alternative Rock": 12,
"Pop": 18,
"Jazz": 32,
},
colors:["#a55ca5","#67b6c7", "#bccd7a","#eb9743"],
titleOptions: {
align: "center",
fill: "black",
font: {
weight: "bold",
size: "18px",
family: "Lato"
}
}
}
);
myBarchart.draw();
下面是结果的样子:

为了添加图例,我们首先需要修改index.html ,看起来像这样:
<html>
<body>
<canvas id="myCanvas" style="background: white;"></canvas>
<legend for="myCanvas"></legend>
<script type="text/javascript" src="script.js"></script>
</body>
</html>
legend 标签将被用作图表图例的占位符。for 属性将图例链接到放置图表的画布上。我们现在需要添加创建图例的代码。我们将在index.js 文件中,在绘制数据系列名称的代码之后进行这项工作。该代码确定了与图表相对应的legend 标签,它将从图表的数据模型中添加类别列表以及相应的颜色。产生的index.js文件将看起来像这样:
var myCanvas = document.getElementById("myCanvas");
myCanvas.width = 500;
myCanvas.height = 500;
var ctx = myCanvas.getContext("2d");
function drawLine(ctx, startX, startY, endX, endY, color) {
ctx.save();
ctx.strokeStyle = color;
ctx.beginPath();
ctx.moveTo(startX, startY);
ctx.lineTo(endX, endY);
ctx.stroke();
ctx.restore();
}
function drawBar(
ctx,
upperLeftCornerX,
upperLeftCornerY,
width,
height,
color
) {
ctx.save();
ctx.fillStyle = color;
ctx.fillRect(upperLeftCornerX, upperLeftCornerY, width, height);
ctx.restore();
}
class BarChart {
constructor(options) {
this.options = options;
this.canvas = options.canvas;
this.ctx = this.canvas.getContext("2d");
this.colors = options.colors;
this.titleOptions = options.titleOptions;
this.maxValue = Math.max(...Object.values(this.options.data));
}
drawGridLines() {
var canvasActualHeight = this.canvas.height - this.options.padding * 2;
var canvasActualWidth = this.canvas.width - this.options.padding * 2;
var gridValue = 0;
while (gridValue <= this.maxValue) {
var gridY =
canvasActualHeight * (1 - gridValue / this.maxValue) +
this.options.padding;
drawLine(
this.ctx,
0,
gridY,
this.canvas.width,
gridY,
this.options.gridColor
);
drawLine(
this.ctx,
15,
this.options.padding / 2,
15,
gridY + this.options.padding / 2,
this.options.gridColor
);
// Writing grid markers
this.ctx.save();
this.ctx.fillStyle = this.options.gridColor;
this.ctx.textBaseline = "bottom";
this.ctx.font = "bold 10px Arial";
this.ctx.fillText(gridValue, 0, gridY - 2);
this.ctx.restore();
gridValue += this.options.gridStep;
}
}
drawBars() {
var canvasActualHeight = this.canvas.height - this.options.padding * 2;
var canvasActualWidth = this.canvas.width - this.options.padding * 2;
var barIndex = 0;
var numberOfBars = Object.keys(this.options.data).length;
var barSize = canvasActualWidth / numberOfBars;
var values = Object.values(this.options.data);
for (let val of values) {
var barHeight = Math.round((canvasActualHeight * val) / this.maxValue);
console.log(barHeight);
drawBar(
this.ctx,
this.options.padding + barIndex * barSize,
this.canvas.height - barHeight - this.options.padding,
barSize,
barHeight,
this.colors[barIndex % this.colors.length]
);
barIndex++;
}
}
drawLabel() {
this.ctx.save();
this.ctx.textBaseline = "bottom";
this.ctx.textAlign = this.titleOptions.align;
this.ctx.fillStyle = this.titleOptions.fill;
this.ctx.font = `${this.titleOptions.font.weight} ${this.titleOptions.font.size} ${this.titleOptions.font.family}`;
let xPos = this.canvas.width / 2;
if (this.titleOptions.align == "left") {
xPos = 10;
}
if (this.titleOptions.align == "right") {
xPos = this.canvas.width - 10;
}
this.ctx.fillText(this.options.seriesName, xPos, this.canvas.height);
this.ctx.restore();
}
drawLegend() {
let pIndex = 0;
let legend = document.querySelector("legend[for='myCanvas']");
let ul = document.createElement("ul");
legend.append(ul);
for (let ctg of Object.keys(this.options.data)) {
let li = document.createElement("li");
li.style.listStyle = "none";
li.style.borderLeft =
"20px solid " + this.colors[pIndex % this.colors.length];
li.style.padding = "5px";
li.textContent = ctg;
ul.append(li);
pIndex++;
}
}
draw() {
this.drawGridLines();
this.drawBars();
this.drawLabel();
this.drawLegend();
}
}
var myBarchart = new BarChart({
canvas: myCanvas,
seriesName: "Vinyl records",
padding: 40,
gridStep: 5,
gridColor: "black",
data: {
"Classical Music": 16,
"Alternative Rock": 12,
Pop: 18,
Jazz: 32
},
colors: ["#a55ca5", "#67b6c7", "#bccd7a", "#eb9743"],
titleOptions: {
align: "center",
fill: "black",
font: {
weight: "bold",
size: "18px",
family: "Lato"
}
}
});
myBarchart.draw();
这将产生一个看起来像这样的最终结果。
试着给图表传递不同的选项,看看它对最终的结果有什么影响。作为一个练习,你能写代码让这些网格线变成多色的吗?
恭喜你
我们已经看到,使用HTML5画布绘制图表其实并不难。它只需要一点数学知识和一点 JavaScript 知识。现在您已经拥有了绘制自己的柱状图所需的一切。