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

680 阅读3分钟

在本教程中,我将向你展示如何使用JavaScript和画布,以饼状图和圆环图的形式显示数字信息。

什么是饼状图?

图表是一种统计工具,用于用图形表示数字数据。饼图将数字数据显示为一个被分成几片的圆。每片的大小与它所代表的数值成正比。

什么是甜甜圈图?

简单地说,甜甜圈图是饼图的一个变种。不同的是,切片是朝着饼的中心切的,这样就只有边缘是可见的。这样一来,图表看起来就像一个甜甜圈,因此得名。

用画布开始绘制

在绘制饼图之前,我们先来看看如何绘制饼图的各个部分。我们将看到我们如何使用画布组件和JavaScript来绘制:

  • 一条线
  • 一条弧线(圆的一部分)
  • 一个充满色彩的形状

要开始使用HTML5画布绘制,我们需要创建一些东西:

  • 一个存放项目文件的文件夹;我们把这个文件夹称为piechart-tutorial
  • piechart-tutorial文件夹中的一个HTML文件index.html。这个文件将包含HTML代码。
  • piechart-tutorial文件夹下有一个JS文件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里面,JS代码将首先获得对画布的引用,然后设置其宽度和高度。要在画布上绘图,我们只需要对其2D上下文的引用,其中包含所有的绘图方法:

var myCanvas = document.getElementById("myCanvas");
myCanvas.width = 300;
myCanvas.height = 300;

var ctx = myCanvas.getContext("2d");

添加一些辅助函数

现在我们已经设置了画布,也有了对绘图画布的引用,让我们定义几个JavaScript函数,在绘制饼图时可以重复使用。我们将在我们的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: 线条的颜色

我们在该函数中做的第一件事是保存当前的上下文。之后,我们通过调用beginPath() 开始画线。这通知了绘图上下文,我们开始在画布上绘制新的东西。我们使用moveTo() 来设置起点,调用lineTo() 来指示终点,然后通过调用stroke() 来进行实际的绘制。

现在让我们看看如何画出一个圆的一部分,也叫弧:

function drawArc(ctx, centerX, centerY, radius, startAngle, endAngle, color){
    ctx.save();
    ctx.strokeStyle = color;
    ctx.beginPath();
    ctx.arc(centerX, centerY, radius, startAngle, endAngle);
    ctx.stroke();
    ctx.restore();
}

drawArc 这个函数需要七个参数:

  1. ctx:对绘图上下文的引用
  2. centerX:圆心的X坐标
  3. centerY:圆心的Y坐标
  4. radius: 线的终点的X坐标
  5. startAngle: 圆的部分开始的起始角度(弧度
  6. endAngle: 圆的部分结束时的弧度角,单位是弧度
  7. color: 弧线的颜色

我们已经看到了如何画线和如何画弧,现在让我们来看看如何画一个彩色的形状。由于我们的目标是绘制一个由切片组成的饼图,所以我们来创建一个绘制饼图的函数:

function drawPieSlice(ctx,centerX, centerY, radius, startAngle, endAngle, fillColor, strokeColor) {
    ctx.save();
    ctx.fillStyle = fillColor;
    ctx.strokeStyle = strokeColor;
    ctx.beginPath();
    ctx.moveTo(centerX, centerY);
    ctx.arc(centerX, centerY, radius, startAngle, endAngle, strokeColor);
    ctx.closePath();
    ctx.fill();
    ctx.restore();
}

drawPieSlice 这个函数需要七个参数:

  1. ctx:对绘图上下文的引用
  2. centerX: 圆心的X坐标
  3. centerY:圆心的Y坐标
  4. radius: 线条端点的X坐标
  5. startAngle: 圆的部分开始的起始角度(弧度
  6. endAngle: 圆的部分结束的角度(弧度)
  7. fillColor:用于填充切片的颜色
  8. strokeColor:用于绘制切片周长的颜色

下面是一个调用三个函数的例子:

drawLine(ctx, 200, 200, 300, 300, "#000");
drawArc(ctx, 250, 250, 150, 0, Math.PI/3, "#000");
drawPieSlice(ctx, 250, 250, 150, Math.PI/2, Math.PI/2 + Math.PI/3, "#F00", "#000");

它将产生这样的结果。

现在我们有了绘制饼图所需的所有工具,让我们看看如何将它们结合起来使用。

绘制饼图

从概念上讲,任何图表都有两个主要部分:

  • 数据模型包含要表示的数字数据,这是以特定于图表类型的格式进行结构化的。
  • 图形表示是数据模型中的数字数据如何根据数学公式形式的一些规则由视觉元素来表示。

饼图数据模型

饼图的数据模型最常见的结构方式是一系列的类别和相应的值,其中每个类别和值都与饼的一个部分相关。

作为一个例子,显示我按流派分类的唱片数量的饼图的数据模型看起来是这样的:

  • 古典音乐:16
  • 另类摇滚:12
  • 流行音乐:18
  • 爵士乐:32

这些数据可以以一个对象的形式传递给我们的饼图绘制类。我们将在后面把它传递给我们的PieChart 对象:

{
    "Classical Music": 16, 
    "Alternative Rock": 12, 
    "Pop": 18, 
    "Jazz": 32,
}

饼图的图形表示法

饼图使用一个圆圈来显示数据模型中的信息,将其划分为若干片断。每个片断对应于数据模型中的一个类别,片断的大小与类别的值成正比。

我收集的88张唱片有四个类别。每个类别将得到一个饼图的切片,与该类别中的乙烯基数量成正比。

但是,我们如何衡量一个片断的大小?这很容易--我们通过切片顶端的角度来衡量。我们只需要知道,全圆对应的角度是360 degrees2 * PI 。因此,半圆对应的角度是180 degPI ,四分之一对应的角度是90 degPI/2 ,以此类推。

为了确定每个类别切片的角度,我们使用了这个公式。

slice angle = 2 * PI * category value / total value

根据这个公式,16张古典音乐唱片的切片角度大约为0.36*PI或64.8度。

让我们开始绘制。为此,我们将使用一个JavaScript类,我们将其命名为Piechart 。构造函数将接收一个options 参数,一个包含以下内容的对象:

  • canvas对画布的引用,我们要在那里绘制饼图
  • data:对一个保存数据模型的对象的引用
  • colors一个数组,用于存放我们要为每个片断使用的颜色
  • seriesName:一个存放图表标题的字符串
  • padding:告诉我们应该在图表中添加多少填充物
  • donutHoleSize: 指定应该从饼图中切出的部分。
  • titleOptions: 一个对象,包含关于标题的字体、大小、对齐方式、颜色等信息。

Piechart 类还包含一个方法,即draw() ,用于实际绘制图表:

var Piechart = function(options){
  constructor(options) {
    this.options = options;
    this.canvas = options.canvas;
    this.ctx = this.canvas.getContext("2d");
    this.colors = options.colors;
    this.titleOptions = options.titleOptions;
    this.totalValue = [...Object.values(this.options.data)].reduce((a, b) => a + b, 0);
    this.radius = Math.min(this.canvas.width / 2, this.canvas.height / 2) - options.padding;
  }

  drawSlices() {
    var colorIndex = 0;
    var startAngle = -Math.PI / 2;

    for (var categ in this.options.data) {
      var val = this.options.data[categ];
      var sliceAngle = (2 * Math.PI * val) / this.totalValue;

      drawPieSlice(
        this.ctx,
        this.canvas.width / 2,
        this.canvas.height / 2,
        this.radius,
        startAngle,
        startAngle + sliceAngle,
        this.colors[colorIndex % this.colors.length]
      );

      startAngle += sliceAngle;
      colorIndex++;
    }
  }
}

该类首先在constructor() 方法中的不同属性中存储传递的options 。它存储了canvas 的引用,并创建了一个绘图上下文,也作为一个类成员存储。然后,它存储了作为选项传递的colors 数组。

我们在构造函数中把我们的对象值转换成一个数组,然后使用reduce()方法来获得所有数据点的总和。这个值后来被用来计算不同片区的角度。我们还在构造函数方法中计算饼图的半径,方法是取画布宽度的一半和画布高度的一半之间的最小值,然后减去padding。减去填充物来计算半径有助于在不同的图表元素之间引入一些空间。

drawSlices() 方法完成饼图的所有实际绘制工作。对于数据模型中的每个类别,我们应用上面提到的公式来计算饼片的角度。最后,我们使用drawPieSlice() 函数,将画布的中心作为切片的中心。在每次绘制类别时,我们也会偏移切片的开始和结束角度,否则切片会重叠。

要使用这个类,我们必须创建一个实例,然后对创建的对象调用drawSlices() 方法:

var myPiechart = new Piechart(
    {
    	canvas: myCanvas,
    	data: {
            "Classical Music": 16,
            "Alternative Rock": 12,
            "Pop": 18,
            "Jazz": 32
        },
        colors: ["#80DEEA", "#FFE082", "#FFAB91", "#CE93D8"]
    }
);
myPiechart.drawSlices();

而结果看起来是这样的:

Pie Chart Without Labels

绘制甜甜圈图

我们已经看到了如何绘制饼状图。我们还知道,甜甜圈图的不同之处在于它的中间有一个洞。我们如何画出这个洞呢?我们可以在饼状图上画一个白圈。

让我们修改Piechart 类的drawSlices() 方法来实现这个目的:

drawSlices() {
    var colorIndex = 0;
    var startAngle = -Math.PI / 2;
    
    for (var categ in this.options.data) {
      var val = this.options.data[categ];
      var sliceAngle = (2 * Math.PI * val) / this.totalValue;
    
      drawPieSlice(
        this.ctx,
        this.canvas.width / 2,
        this.canvas.height / 2,
        this.radius,
        startAngle,
        startAngle + sliceAngle,
        this.colors[colorIndex % this.colors.length]
      );
    
      startAngle += sliceAngle;
      colorIndex++;
    }
    
    if (this.options.doughnutHoleSize) {
      drawPieSlice(
        this.ctx,
        this.canvas.width / 2,
        this.canvas.height / 2,
        this.options.doughnutHoleSize * this.radius,
        0,
        2 * Math.PI,
        "#FFF",
        "#FFF"
      );
    
      drawArc(
        this.ctx,
        this.canvas.width / 2,
        this.canvas.height / 2,
        this.options.doughnutHoleSize * this.radius,
        0,
        2 * Math.PI,
        "#000"
      );
    }
}

添加的代码在options 参数中寻找一个成员变量doughnutHoleSize 。如果这个变量在选项中不存在,那么代码将像以前一样绘制饼图,但是如果它存在,那么将绘制一个白色的圆,圆心与饼图相同。

圆的半径由饼图半径和doughnutHoleSize 的值决定。这应该是一个介于0和1之间的数字,其中0会导致饼图,任何高于0的值都会导致圆环图的孔越来越大,1会使图表不可见。

为了画出一个洞口为图表一半大小的甜甜圈图,我们需要使用0.5的doughnutHoleSize ,并进行以下调用:

var myDougnutChart = new Piechart(
    {
    	canvas:myCanvas,
    	data:myVinyls,
    	colors:["#fde23e","#f16e23", "#57d9ff","#937e88"],
    	doughnutHoleSize:0.5
    }
);

myDougnutChart.drawSlices();

下面是结果:

Donut Chart Without Labels

添加标签和图表图例

我们的饼图和甜甜圈图看起来很不错,但我们可以通过添加两样东西使它们变得更好:

  • 价值标签:显示每块饼所对应的百分比
  • 图表图例:显示图表中的类别及其对应的颜色

通常,与切片相关的数值表示为百分比值,计算方法是100 * value associated with a slice / total value ,整个圆圈代表100%

例如,在我们的样本数据中,有古典音乐的唱片大约代表26% 。如果能把这个值直接写在相应的切片上,那就更好了。为了做到这一点,我们将使用绘图上下文的fillText(text,x,y) 函数。这个函数需要三个参数:文本以及xy 坐标。

我们如何计算出放置文本的xy 坐标?我们必须利用一些几何知识和一种叫做极坐标的东西。基本上,极坐标使用一个半径和一个角度来定义一个点的位置。我们将使用的两个公式是:

x = R * cos(angle)

y = R * sin(angle)

我们将应用这两个公式,把文字放在饼图半径的一半和每个饼片的角度的一半处。要做到这一点,我们需要在我们的Piechart 类中添加另一个名为drawLabels() 的方法:

drawLabels() {
    var colorIndex = 0;
    var startAngle = -Math.PI / 2;
    for (var categ in this.options.data) {
      var val = this.options.data[categ];
      var sliceAngle = (2 * Math.PI * val) / this.totalValue;
      var labelX =
        this.canvas.width / 2 +
        (this.radius / 2) * Math.cos(startAngle + sliceAngle / 2);
      var labelY =
        this.canvas.height / 2 +
        (this.radius / 2) * Math.sin(startAngle + sliceAngle / 2);
    
      if (this.options.doughnutHoleSize) {
        var offset = (this.radius * this.options.doughnutHoleSize) / 2;
        labelX =
          this.canvas.width / 2 +
          (offset + this.radius / 2) * Math.cos(startAngle + sliceAngle / 2);
        labelY =
          this.canvas.height / 2 +
          (offset + this.radius / 2) * Math.sin(startAngle + sliceAngle / 2);
      }
    
      var labelText = Math.round((100 * val) / this.totalValue);
      this.ctx.fillStyle = "black";
      this.ctx.font = "32px Khand";
      this.ctx.fillText(labelText + "%", labelX, labelY);
      startAngle += sliceAngle;
    }
}

这段代码会翻阅每一片,计算百分比,计算位置,并使用fillText() 方法在图表上绘制。我们使用了fillStyle 属性来设置文本颜色为黑色,使用font 属性来设置标签的大小和字体家族。还需要注意的是,如果图表是一个甜甜圈图表,并且设置了doughnutHoleSize ,那么标签将被推向图表的边缘,使其在甜甜圈片上居中。

下面是生成的带有数值标签的图表的样子:

Pie and Donut Charts with Labels

为了完成我们的图表,我们将添加的最后一件事是图表图例。我们的图表图例将显示我们的数据模型的类别和用于相应切片的颜色。首先,我们必须对我们的index.html 文件做一些修改,添加一个<div> 标签,该标签将存储我们的图例元素:

<html>
<body>
    <canvas id="myCanvas"></canvas>
    <div for="myCanvas"></div>
    <script type="text/javascript" src="script.js"></script>
</body>
</html>

然后在script.js中,我们添加代码,创建图例元素的内容。我们在Piechart 类的drawLegend() 方法中这样做:

drawLegend() {
    let pIndex = 0;
    let legend = document.querySelector("div[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++;
    }
}

该代码寻找一个通过options 参数传递的legend 元素。如果提供了一个,这个元素就会被填入HTML代码,其中包含一个彩色的方框和数据模型类别的名称。

我们的图表也将有一个标题来描述图表应该向我们展示什么。我们可以通过在我们的PieChart 类中添加另一个名为drawTitle() 的方法来做到这一点。图表标题的配置选项将取自传递给PieChart 类的titleOptions 对象:

drawTitle() {
    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();
}

为了自己的方便,我们还可以做一件事,就是定义一个draw() 方法,调用类的所有其他方法来绘制整个图表:

draw() {
    this.drawSlices();
    this.drawLabels();
    this.drawTitle();
    this.drawLegend();
}

我们还需要对我们的饼图的绘制方式做一个改变,像这样调用饼图:

var myPiechart = new PieChart({
  canvas: myCanvas,
  seriesName: "Vinyl records",
  padding: 40,
  doughnutHoleSize: 0.4,
  data: {
    "Classical Music": 16,
    "Alternative Rock": 12,
    "Pop": 18,
    "Jazz": 32
  },
  colors: ["#80DEEA", "#FFE082", "#FFAB91", "#CE93D8"],
  titleOptions: {
    align: "center",
    fill: "black",
    font: {
      weight: "bold",
      size: "18px",
      family: "Lato"
    }
  }
});

myPiechart.draw();

而这是所产生的图表和图表图例。

恭喜你

我们已经看到,使用HTML5画布绘制图表其实并不难。它只需要一点数学知识和一点 JavaScript 知识。您现在已经拥有了绘制自己的饼图和圆环图所需的一切。