数据可视化系列之 Canvas 绘制折线图表

428 阅读3分钟

pexels-gabriel-garcia-1263144-2403629.jpg

引言

数据可视化是将复杂数据以图形化方式呈现的一门艺术,它可以帮助我们更直观地理解数据。 在上一篇中,我们整理了 Canvas 的使用手册,现在我们用一个折线图表来对 API 进行实际使用,为了加深肌肉记忆

案例介绍截图

image.png

在线访问入口

  • 本案例的目标是绘制一个展示每月跑步里程趋势的折线图。使用 Canvas API 来绘制图形、设置样式、处理数据点,并添加坐标轴和数据点标注。
  • 基于数据驱动视图理念,先配置数据
  • 设置边距配置对象,让图表自适应
  • 文章末尾有完整代码
const dataPoints = [
  { month: '一月', value: 50 },
  { month: '二月', value: 450 },
  { month: '三月', value: 300 },
  { month: '四月', value: 900 },
  { month: '五月', value: 300 },
  { month: '六月', value: 250 },
  { month: '七月', value: 600 },
]
let padding = { top: 50, bottom: 20, left: 50, right: 10 }
let title = '每月跑步里程趋势'

DOM 创建

首先,创建一个 HTML 文件,并在<head>标签内设置字符集和标题,在<body>标签内定义一个<canvas>元素,并为其添加样式。

<canvas id="lineChart" width="800" height="400"></canvas>

绘制标题

在 Canvas 上绘制标题,使用fillText方法,并设置字体和颜色。

ctx.font = '20px Arial'
ctx.fillStyle = '#ff00ff'
ctx.fillText(title, 300, 40)

数据准备

定义一个包含月份和对应数值的对象数组,这些数据点将用于绘制折线图。

const dataPoints = [
  { month: '一月', value: 50 },
  // ... 其他数据点
]

坐标轴和刻度设置

计算坐标轴的刻度和标签,使用map方法从数据点生成 X 轴标签,为 Y 轴创建刻度。

const xAxisLabel = dataPoints.map(point => point.month)
// ...
const yAxisLabel = new Array(yItem + 1).fill().map((_, i) => Math.ceil((yMax / yItem) * i))

绘制坐标轴

使用moveTolineTo方法绘制坐标轴,并设置线条样式。

ctx.beginPath()
// 绘制X轴和Y轴
ctx.moveTo(padding.left, padding.top)
// ...
ctx.strokeStyle = '#000'
ctx.stroke()

绘制刻度线和标签

为 X 轴和 Y 轴绘制刻度线,并添加标签文本。

// X轴刻度线和标签
ctx.moveTo(xPosition, canvas.height - padding.bottom)
ctx.lineTo(xPosition, canvas.height - padding.bottom - lineHeight)
ctx.strokeText(point.month, xPosition, canvas.height - padding.bottom + 15)

// Y轴刻度线和标签
ctx.moveTo(padding.left, yPosition)
ctx.lineTo(padding.left - lineHeight, yPosition)
ctx.strokeText(label, padding.left - 25, yPosition)

折线图绘制

遍历数据点,使用beginPathmoveTolineTostroke方法绘制折线。

ctx.beginPath()
ctx.strokeStyle = '#00f'
ctx.lineWidth = 2
// ...
ctx.moveTo(padding.left, ydot)
// ...
ctx.lineTo(x, y)
// ...
ctx.stroke()

数据点绘制

在折线图上绘制每个数据点,使用arc方法绘制圆形。

ctx.fillStyle = '#f00'
dataPoints.forEach((point, index) => {
  // ...
  ctx.beginPath()
  ctx.arc(x, y, 5, 0, 2 * Math.PI)
  ctx.fill()
})

完整代码

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>Canvas 折线图示例</title>
    <style>
      canvas {
        border: 1px solid black;
      }
    </style>
  </head>
  <body>
    <canvas id="lineChart" width="800" height="400"></canvas>
    <script>
      document.addEventListener('DOMContentLoaded', function () {
        const canvas = document.getElementById('lineChart')
        const ctx = canvas.getContext('2d')

        // 数据设置
        const dataPoints = [
          { month: '一月', value: 50 },
          { month: '二月', value: 450 },
          { month: '三月', value: 300 },
          { month: '四月', value: 900 },
          { month: '五月', value: 300 },
          { month: '六月', value: 250 },
          { month: '七月', value: 600 },
        ]
        let padding = { top: 50, bottom: 20, left: 50, right: 10 }
        let title = '每月跑步里程趋势'

        // 绘制标题
        ctx.save()
        ctx.font = '20px Arial'
        ctx.fillStyle = '#ff00ff'
        ctx.fillText(title, 300, 40)
        ctx.restore()

        // 设置刻度和标签
        const xAxisLabel = dataPoints.map(point => point.month)
        const xScale = (canvas.width - padding.left - padding.right) / dataPoints.length

        let yItem = 7
        let yHeight = canvas.height - padding.top - padding.bottom
        let yMax = Math.max(...dataPoints.map(dp => dp.value))
        const ySpace = yHeight / yItem
        const yAxisLabel = new Array(yItem + 1).fill().map((_, i) => Math.ceil((yMax / yItem) * i))
        const yScale = canvas.height / Math.max(...dataPoints.map(dp => dp.value))
        let lineHeight = 15

        // 绘制坐标轴
        ctx.beginPath()
        ctx.moveTo(padding.left, padding.top)
        ctx.lineTo(padding.left, canvas.height - padding.bottom)
        ctx.lineTo(canvas.width - padding.right, canvas.height - padding.bottom)
        ctx.strokeStyle = '#000'
        ctx.stroke()

        // 绘制 X 轴刻度线和标签
        ctx.textBaseline = 'bottom'
        ctx.textAlign = 'center'
        dataPoints.forEach((point, index) => {
          const xPosition = padding.left + index * xScale
          ctx.moveTo(xPosition, canvas.height - padding.bottom)
          ctx.lineTo(xPosition, canvas.height - padding.bottom - lineHeight) // 高度15的线段
          ctx.strokeText(point.month, xPosition, canvas.height - padding.bottom + 15)
        })

        // 绘制 Y 轴刻度线和标签
        ctx.textBaseline = 'middle'
        ctx.textAlign = 'right'
        yAxisLabel.forEach((label, index) => {
          const yPosition = canvas.height - padding.bottom - index * ySpace // 底部留了 padding.bottom
          ctx.moveTo(padding.left, yPosition)
          ctx.lineTo(padding.left - lineHeight, yPosition)
          ctx.strokeText(label, padding.left - 25, yPosition)
        })
        ctx.stroke()

        const getYDot = val => {
          return canvas.height - yHeight * (val / yMax) - padding.bottom
        }
        // 绘制折线图
        ctx.beginPath()
        ctx.strokeStyle = '#00f'
        ctx.lineWidth = 2
        let ydot = getYDot(dataPoints[0].value)
        ctx.moveTo(padding.left, ydot)

        dataPoints.forEach((point, index) => {
          let ydot = getYDot(point.value)
          const x = padding.left + index * xScale
          const y = ydot
          console.log(x, y)
          ctx.lineTo(x, y)
        })
        ctx.stroke()

        // 绘制数据点
        ctx.fillStyle = '#f00'
        dataPoints.forEach((point, index) => {
          const x = padding.left + index * xScale
          const y = getYDot(point.value)
          ctx.beginPath()
          ctx.arc(x, y, 5, 0, 2 * Math.PI)
          ctx.fill()
        })
      })
    </script>
  </body>
</html>

结语

通过本案例的实践,我们不仅掌握了 Canvas 的基本使用方法,还学习了如何将数据点转换为可视化图形。折线图作为一种常见的数据可视化形式,能够帮助我们快速理解数据随时间变化的趋势。Canvas 的强大功能,结合 JavaScript 的灵活性,为数据可视化提供了无限可能。