如何使用JavaScript和HTML5 Canvas绘制条形图(附实例)

885 阅读8分钟

在之前的教程中,我们介绍了如何使用HTML5画布绘制饼状图或甜甜圈图。在本教程中,我将向您展示如何使用JavaScript和HTML5画布,通过使用条形图以图形方式显示数据。

什么是柱状图?

柱状图是用于表示数字数据的非常常见的工具。从财务报告到PowerPoint演示文稿再到信息图表,柱状图都被经常使用,因为它们提供了一种非常容易理解的数字数据视图。

条形图使用条形来表示数字数据,条形是矩形,其宽度或高度与它们所代表的数字数据成正比。

条形图有许多类型,例如:

  • 水平条形图和垂直条形图,取决于图表的方向
  • 堆叠式条形图或经典的条形图,用于表示多个系列的数据
  • 二维或三维条形图

条形图的组成部分是什么?

让我们来看看构成柱状图的组成部分,无论其类型如何:

Bar Chart Infographic

  • 图表数据:这些是一组数字和相关的类别,由图表来表示。
  • 数据系列的名称(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 这个函数需要六个参数:

  1. ctx:对绘图环境的引用
  2. startX: 线条起点的X坐标
  3. startY线条起点的Y坐标
  4. endX: 线条终点的X坐标
  5. endY线条终点的Y坐标
  6. 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 这个函数需要六个参数:

  1. ctx:对绘图环境的引用
  2. upperLeftCornerX: 条形的左上角的X坐标
  3. upperLeftCornerY: 条形图左上角的X坐标
  4. width条形图的宽度
  5. height条形图的高度
  6. 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() 方法。这将绘制网格线和网格标记。

canvasActualHeightcanvasActualWidth 变量存储了画布的高度和宽度,并使用通过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应该产生这样的结果:

Creating a Simple Bar Chart

添加数据系列名称和图表图例

为了在图表下面添加数据系列名称,我们需要在我们的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();

下面是结果的样子:

Adding a Title to Bar Chart

为了添加图例,我们首先需要修改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 知识。现在您已经拥有了绘制自己的柱状图所需的一切。