# 分步指南

遵循本指南,熟悉 Chart.js 的所有主要概念:图表类型和元素、数据集、自定义、插件、组件和树状摇动。不要犹豫,点击文本中的链接。

我们将从头开始构建一个包含几个图表的 Chart.js 数据可视化。

result

# 使用 Chart.js 构建新应用程序

在新的文件夹中,创建一个名为 package.json 的文件,内容如下:

{
  "name": "chartjs-example",
  "version": "1.0.0",
  "license": "MIT",
  "scripts": {
    "dev": "parcel src/index.html",
    "build": "parcel build src/index.html"
  },
  "devDependencies": {
    "parcel": "^2.6.2"
  },
  "dependencies": {
    "@cubejs-client/core": "^0.31.0",
    "chart.js": "^4.0.0"
  }
}

现代前端应用程序通常使用 JavaScript 模块打包程序,因此我们选择了 Parcel (在新窗口中打开) 作为一款很棒的零配置构建工具。我们还安装了 Chart.js v4 和一个用于 Cube (在新窗口中打开) 的 JavaScript 客户端,这是一个用于数据应用程序的开源 API,我们将使用它来获取真实世界的数据(稍后会详细介绍)。

运行 npm installyarn installpnpm install 来安装依赖项,然后创建 src 文件夹。在该文件夹中,我们需要一个非常简单的 index.html 文件

<!doctype html>
<html lang="en">
  <head>
    <title>Chart.js example</title>
  </head>
  <body>
    <!-- <div style="width: 500px;"><canvas id="dimensions"></canvas></div><br/> -->
    <div style="width: 800px;"><canvas id="acquisitions"></canvas></div>
    <!-- <script type="module" src="dimensions.js"></script> -->
    <script type="module" src="acquisitions.js"></script>
  </body>
</html>

如您所见,Chart.js 需要最少的标记:一个带有 idcanvas 标签,我们稍后将使用它来引用图表。默认情况下,Chart.js 图表是 响应式 的,并占据整个封闭容器。因此,我们设置 div 的宽度来控制图表宽度。

最后,让我们创建 src/acquisitions.js 文件,内容如下:

import Chart from 'chart.js/auto'
(async function() {
  const data = [
    { year: 2010, count: 10 },
    { year: 2011, count: 20 },
    { year: 2012, count: 15 },
    { year: 2013, count: 25 },
    { year: 2014, count: 22 },
    { year: 2015, count: 30 },
    { year: 2016, count: 28 },
  ];
  new Chart(
    document.getElementById('acquisitions'),
    {
      type: 'bar',
      data: {
        labels: data.map(row => row.year),
        datasets: [
          {
            label: 'Acquisitions by year',
            data: data.map(row => row.count)
          }
        ]
      }
    }
  );
})();

让我们来看看这段代码。

  • 我们从特殊的 chart.js/auto 路径中导入 Chart,它是 Chart.js 的主要类。它加载 所有可用的 Chart.js 组件 (这非常方便),但禁止树状摇动。我们稍后将解决这个问题。
  • 我们实例化一个新的 Chart 实例,并提供两个参数:要渲染图表的画布元素和选项对象。
  • 我们只需要提供一个图表类型(bar)并提供 data,它由 labels(通常是数据点的数值或文本描述)和 datasets 数组组成(Chart.js 支持大多数图表类型的多个数据集)。每个数据集都用 label 指定,并包含一个数据点数组。
  • 目前,我们只有几个虚拟数据的条目。因此,我们提取 yearcount 属性以生成唯一数据集中 labels 和数据点的数组。

是时候用 npm run devyarn devpnpm dev 运行示例并导航到 localhost:1234 (在新窗口中打开) 在您的网页浏览器中。

result

只需几行代码,我们就得到了一个功能丰富的图表:一个 图例网格线刻度工具提示(悬停时显示)。刷新网页几次,您会发现图表也是 动画 的。尝试点击“按年分列的收购”标签,您会发现您还可以切换数据集的可见性(当您有多个数据集时尤其有用)。

# 简单自定义

让我们看看如何自定义 Chart.js 图表。首先,让我们关闭动画,这样图表会立即出现。其次,让我们隐藏图例和工具提示,因为我们只有一个数据集和非常简单的数据。

用以下代码段替换 src/acquisitions.js 中的 new Chart(...); 调用

  new Chart(
    document.getElementById('acquisitions'),
    {
      type: 'bar',
      options: {
        animation: false,
        plugins: {
          legend: {
            display: false
          },
          tooltip: {
            enabled: false
          }
        }
      },
      data: {
        labels: data.map(row => row.year),
        datasets: [
          {
            label: 'Acquisitions by year',
            data: data.map(row => row.count)
          }
        ]
      }
    }
  );

如您所见,我们已向第二个参数添加了 options 属性——这就是您指定 Chart.js 的各种自定义选项的方式。通过 animation 提供的布尔标志 禁用动画。大多数图表范围的选项(例如 响应式设备像素比)都是这样配置的。

图例和工具提示使用在各自部分中提供的布尔标志隐藏。请注意,一些 Chart.js 功能被提取到插件中:自包含的、独立的代码部分。其中一些作为 Chart.js 发行版 (在新窗口中打开) 的一部分提供,其他插件由独立维护,可以在 awesome 列表 (在新窗口中打开) 中找到,该列表包含插件、框架集成和额外的图表类型。

您应该能够在浏览器中看到更新后的最小图表。

# 真实世界的数据

使用硬编码的、有限大小的、不切实际的数据,很难展示 Chart.js 的全部潜力。让我们快速连接到数据 API,使我们的示例应用程序更接近生产用例。

让我们创建名为 src/api.js 的文件,内容如下:

import { CubejsApi } from '@cubejs-client/core';
const apiUrl = 'https://heavy-lansford.gcp-us-central1.cubecloudapp.dev/cubejs-api/v1';
const cubeToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjEwMDAwMDAwMDAsImV4cCI6NTAwMDAwMDAwMH0.OHZOpOBVKr-sCwn8sbZ5UFsqI3uCs6e4omT7P6WVMFw';
const cubeApi = new CubejsApi(cubeToken, { apiUrl });
export async function getAquisitionsByYear() {
  const acquisitionsByYearQuery = {
    dimensions: [
      'Artworks.yearAcquired',
    ],
    measures: [
      'Artworks.count'
    ],
    filters: [ {
      member: 'Artworks.yearAcquired',
      operator: 'set'
    } ],
    order: {
      'Artworks.yearAcquired': 'asc'
    }
  };
  const resultSet = await cubeApi.load(acquisitionsByYearQuery);
  return resultSet.tablePivot().map(row => ({
    year: parseInt(row['Artworks.yearAcquired']),
    count: parseInt(row['Artworks.count'])
  }));
}
export async function getDimensions() {
  const dimensionsQuery = {
    dimensions: [
      'Artworks.widthCm',
      'Artworks.heightCm'
    ],
    measures: [
      'Artworks.count'
    ],
    filters: [
      {
        member: 'Artworks.classification',
        operator: 'equals',
        values: [ 'Painting' ]
      },
      {
        member: 'Artworks.widthCm',
        operator: 'set'
      },
      {
        member: 'Artworks.widthCm',
        operator: 'lt',
        values: [ '500' ]
      },
      {
        member: 'Artworks.heightCm',
        operator: 'set'
      },
      {
        member: 'Artworks.heightCm',
        operator: 'lt',
        values: [ '500' ]
      }
    ]
  };
  const resultSet = await cubeApi.load(dimensionsQuery);
  return resultSet.tablePivot().map(row => ({
    width: parseInt(row['Artworks.widthCm']),
    height: parseInt(row['Artworks.heightCm']),
    count: parseInt(row['Artworks.count'])
  }));
}

让我们看看那里发生了什么。

  • 我们 import 了用于 Cube (在新窗口中打开) 的 JavaScript 客户端库,使用 API URL(apiUrl)和身份验证令牌(cubeToken)配置它,最后实例化客户端(cubeApi)。
  • Cube API 托管在 Cube Cloud (在新窗口中打开) 中,并连接到具有 公开数据集 (在新窗口中打开) 的数据库,该数据集包含大约 140,000 条记录,代表了位于美国纽约的 现代艺术博物馆 (在新窗口中打开) 馆藏的所有艺术品。当然,这比我们现在拥有的数据集更接近真实世界。
  • 我们定义了两个异步函数来从 API 获取数据:getAquisitionsByYeargetDimensions。第一个函数返回按收购年份分类的艺术品数量,另一个函数返回每个宽度-高度对的艺术品数量(我们将为另一个图表使用它)。
  • 让我们看一下 getAquisitionsByYear。首先,我们在 acquisitionsByYearQuery 变量中创建一个声明式的 JSON 查询。如您所见,我们指定了对于每个 yearAcquired,我们希望获得 count 艺术品数量;yearAcquired 必须设置(即不为 undefined);结果集将按 yearAcquired 升序排序。
  • 其次,我们通过调用 cubeApi.load 获取 resultSet,并将其映射到具有所需 yearcount 属性的对象数组。

现在,让我们将真实世界数据传递到图表中。请对 src/acquisitions.js 进行以下更改:添加导入并替换 data 变量的定义。

import { getAquisitionsByYear } from './api'
// ...
const data = await getAquisitionsByYear();

完成!现在,我们的图表使用真实世界数据看起来是这样的。看来 1964 年、1968 年和 2008 年发生了一些有趣的事情!

result

我们完成了条形图。让我们尝试另一种 Chart.js 图表类型。

# 进一步定制

Chart.js 支持许多常见的图表类型。

例如,气泡图 允许同时显示数据的三维:xy 轴上的位置代表两个维度,第三个维度由单个气泡的大小表示。

要创建图表,请停止正在运行的应用程序,然后转到 src/index.html,并取消注释以下两行

<div style="width: 500px;"><canvas id="dimensions"></canvas></div><br/>
<script type="module" src="dimensions.js"></script>

然后,使用以下内容创建 src/dimensions.js 文件

import Chart from 'chart.js/auto'
import { getDimensions } from './api'
(async function() {
  const data = await getDimensions();
  new Chart(
    document.getElementById('dimensions'),
    {
      type: 'bubble',
      data: {
        labels: data.map(x => x.year),
        datasets: [
          {
            label: 'Dimensions',
            data: data.map(row => ({
              x: row.width,
              y: row.height,
              r: row.count
            }))
          }
        ]
      }
    }
  );
})();

可能一切都很直观:我们从 API 获取数据并使用 bubble 类型渲染一个新图表,将数据的三个维度作为 xyr(半径)属性传递。

现在,使用 rm -rf .parcel-cache 重置缓存,并使用 npm run devyarn devpnpm dev 再次启动应用程序。现在我们可以查看新图表

result

好吧,它看起来不太好看。

首先,图表不是正方形。艺术品的宽度和高度同等重要,因此我们希望将图表的宽度也设为与高度相等。默认情况下,Chart.js 图表具有 纵横比 为 1(对于所有径向图表,例如圆环图)或 2(对于其他所有图表)。让我们修改图表的纵横比

// ...
	new Chart(
    document.getElementById('dimensions'),
    {
      type: 'bubble',
      options: {
        aspectRatio: 1,
      },
// ...

现在看起来好多了

result

然而,它仍然不是理想的。水平轴从 0 跨度到 500,而垂直轴从 0 跨度到 450。默认情况下,Chart.js 会自动调整轴的范围(最小值和最大值),以适应数据集中提供的值,因此图表“适合”您的数据。显然,MoMa 收藏没有高度在 450 到 500 厘米范围内的艺术品。让我们修改图表的 轴配置 来解决这个问题

// ...
  new Chart(
    document.getElementById('dimensions'),
    {
      type: 'bubble',
      options: {
        aspectRatio: 1,
        scales: {
          x: {
            max: 500
          },
          y: {
            max: 500
          }
        }
      },
// ...

太好了!看看更新后的图表

result

然而,还有一个不足:这些数字是什么?不太明显的是单位是厘米。让我们应用 自定义刻度格式 到两个轴,以便使事情更清楚。我们将提供一个回调函数,该函数将在格式化每个刻度值时被调用。以下是更新后的轴配置

// ...
  new Chart(
    document.getElementById('dimensions'),
    {
      type: 'bubble',
      options: {
        aspectRatio: 1,
        scales: {
          x: {
            max: 500,
            ticks: {
              callback: value => `${value / 100} m`
            }
          },
          y: {
            max: 500,
            ticks: {
              callback: value => `${value / 100} m`
            }
          }
        }
      },
// ...

完美,现在我们在两个轴上都有正确的单位

result

# 多个数据集

Chart.js 独立绘制每个数据集,并允许为它们应用自定义样式。

看一下图表:有一条明显的气泡“线”,具有相等的 xy 坐标,代表正方形的艺术品。将这些气泡放入它们自己的数据集中并用不同的颜色绘制它们会很酷。此外,我们可以将“更高”的艺术品与“更宽”的艺术品分开,并用不同的颜色绘制它们。

以下是我们的做法。将 datasets 替换为以下代码

// ...
        datasets: [
          {
            label: 'width = height',
            data: data
              .filter(row => row.width === row.height)
              .map(row => ({
                x: row.width,
                y: row.height,
                r: row.count
              }))
          },
          {
            label: 'width > height',
            data: data
              .filter(row => row.width > row.height)
              .map(row => ({
                x: row.width,
                y: row.height,
                r: row.count
              }))
          },
          {
            label: 'width < height',
            data: data
              .filter(row => row.width < row.height)
              .map(row => ({
                x: row.width,
                y: row.height,
                r: row.count
              }))
          }
        ]
// ..

如您所见,我们定义了三个具有不同标签的数据集。每个数据集都获得了使用 filter 提取的数据切片。现在它们在视觉上是不同的,而且您已经知道,您可以独立地切换它们的可见性。

result

在这里,我们依赖于默认的颜色调色板。但是,请记住,每种图表类型都支持许多 数据集选项,您可以随意对其进行自定义。

# 插件

自定义 Chart.js 图表的另一种(非常强大!)方法是使用插件。您可以在 插件目录 (opens new window) 中找到一些插件,或者创建您自己的临时插件。在 Chart.js 生态系统中,使用插件来微调图表是惯例,也是预期行为。例如,您可以使用简单的临时插件来自定义 画布背景添加边框。让我们尝试一下后者。

插件具有 广泛的 API,但简而言之,插件被定义为一个对象,该对象具有一个 name 和一个或多个在扩展点中定义的回调函数。在 src/dimensions.js 中,将以下代码段插入 new Chart(...); 调用之前并替换它

// ...
  const chartAreaBorder = {
    id: 'chartAreaBorder',
    beforeDraw(chart, args, options) {
      const { ctx, chartArea: { left, top, width, height } } = chart;
      ctx.save();
      ctx.strokeStyle = options.borderColor;
      ctx.lineWidth = options.borderWidth;
      ctx.setLineDash(options.borderDash || []);
      ctx.lineDashOffset = options.borderDashOffset;
      ctx.strokeRect(left, top, width, height);
      ctx.restore();
    }
  };
  new Chart(
    document.getElementById('dimensions'),
    {
      type: 'bubble',
      plugins: [ chartAreaBorder ],
      options: {
        plugins: {
          chartAreaBorder: {
            borderColor: 'red',
            borderWidth: 2,
            borderDash: [ 5, 5 ],
            borderDashOffset: 2,
          }
        },
        aspectRatio: 1,
// ...

如您所见,在这个 chartAreaBorder 插件中,我们获取画布上下文,保存其当前状态,应用样式,在图表区域周围绘制矩形,然后恢复画布状态。我们还在 plugins 中传递插件,因此它只应用于此特定图表。我们还在 options.plugins.chartAreaBorder 中传递插件选项;我们当然可以在插件源代码中对其进行硬编码,但这样更具可重用性。

我们的气泡图现在看起来更漂亮了

result

# 树摇

在生产环境中,我们努力尽可能少地发布代码,这样最终用户可以更快地加载我们的数据应用程序,并获得更好的体验。为此,我们需要应用 树摇 (opens new window),这是一个用于从 JavaScript 包中删除未用代码的术语。

Chart.js 通过其组件设计完全支持树摇。您可以一次注册所有 Chart.js 组件(在进行原型设计时很方便),并将其与应用程序捆绑在一起。或者,您也可以只注册必要的组件,并获得一个最小化的包,大小更小。

让我们检查一下我们的示例应用程序。包大小是多少?您可以停止应用程序并运行 npm run buildyarn buildpnpm build。片刻之后,您将得到类似以下内容

% yarn build
yarn run v1.22.17
$ parcel build src/index.html
✨ Built in 88ms
dist/index.html              381 B   164ms
dist/index.74a47636.js   265.48 KB   1.25s
dist/index.ba0c2e17.js       881 B    63ms
✨ Done in 0.51s.

我们可以看到,Chart.js 和其他依赖项被捆绑在一个 265 KB 的文件中。

为了减小包大小,我们需要对 src/acquisitions.jssrc/dimensions.js 进行一些更改。首先,我们需要从这两个文件中删除以下导入语句:import Chart from 'chart.js/auto'

相反,让我们只加载必要的组件,并使用 Chart.register(...) 将它们“注册”到 Chart.js 中。以下是我们在 src/acquisitions.js 中需要的内容

import {
  Chart,
  Colors,
  BarController,
  CategoryScale,
  LinearScale,
  BarElement,
  Legend
} from 'chart.js'
Chart.register(
  Colors,
  BarController,
  BarElement,
  CategoryScale,
  LinearScale,
  Legend
);

以下是 src/dimensions.js 的代码片段

import {
  Chart,
  Colors,
  BubbleController,
  CategoryScale,
  LinearScale,
  PointElement,
  Legend
} from 'chart.js'
Chart.register(
  Colors,
  BubbleController,
  PointElement,
  CategoryScale,
  LinearScale,
  Legend
);

您可以看到,除了 Chart 类之外,我们还加载了图表类型的控制器、刻度和其他图表元素(例如,条形或点)。您可以在 文档 中查看所有可用的组件。

或者,您可以遵循控制台中 Chart.js 的建议。例如,如果您忘记为条形图导入 BarController,您将在浏览器控制台中看到以下消息

Unhandled Promise Rejection: Error: "bar" is not a registered controller.

在为生产环境准备应用程序时,请务必仔细检查来自 chart.js/auto 的导入。只有一个这样的导入就足以有效地禁用树摇。

现在,让我们再次检查一下我们的应用程序。运行 yarn build,您将得到类似以下内容

% yarn build
yarn run v1.22.17
$ parcel build src/index.html
✨ Built in 88ms
dist/index.html              381 B   176ms
dist/index.5888047.js    208.66 KB   1.23s
dist/index.dcb2e865.js       932 B    58ms
✨ Done in 0.51s.

通过导入和注册仅选择的组件,我们已经删除了超过 56 KB 的不必要代码。考虑到其他依赖项在包中大约占 50 KB,树摇有助于从示例应用程序的包中删除约 25% 的 Chart.js 代码。

# 下一步

现在您已经熟悉 Chart.js 的所有主要概念:图表类型和元素、数据集、自定义、插件、组件和树摇。

请随时查看文档中的许多 图表示例,并查看 Chart.js 插件和附加图表类型的出色列表 (opens new window) 以及 框架集成 (opens new window)(例如,React、Vue、Svelte 等)。此外,请随时加入 Chart.js Discord (opens new window) 并关注 Twitter 上的 Chart.js (opens new window)

祝您使用 Chart.js 构建愉快,一切顺利!

上次更新: 2024 年 5 月 17 日下午 12:33:38