Chart-js-学习指南-全-

Chart.js 学习指南(全)

原文:zh.annas-archive.org/md5/627a099052505a6268dd41b5e05f2886

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

学习 Chart.js将使数据密集型网站的视觉化变得简单且吸引人。我将解释如何将复杂的数据简化、易于访问和直观,以便您的用户能够更好地理解您的网站。

本书是创建和发布您自己的交互式数据可视化项目的实用指南。

阅读本书后,您将能够使用 Chart.js 创建美丽的网络图表。

本书面向对象

本书是为希望为网络创建交互式数据可视化的设计师和艺术家而编写的。

基本的 HTML、CSS 和 JavaScript 知识将大有裨益。

要充分利用这本书

我建议您阅读第一章,以确保您对 Chart.js 的基本概念和基础原理有所了解。在接下来的章节中,我们将看到 Chart.js 所需的设置以及本书将教授的各种视觉表示技术及其用途。将会有关于使用不同图表类型的教程,并且我们将探索它们的交互性。

下载示例代码文件

您可以从www.packt.com的账户下载本书的示例代码文件。如果您在其他地方购买了本书,您可以访问www.packt.com/support并注册,以便将文件直接通过电子邮件发送给您。

您可以通过以下步骤下载代码文件:

  1. www.packt.com登录或注册。

  2. 选择支持选项卡。

  3. 点击代码下载与勘误。

  4. 在搜索框中输入书名,并遵循屏幕上的说明。

文件下载后,请确保使用最新版本的以下软件解压缩或提取文件夹:

  • WinRAR/7-Zip for Windows

  • Zipeg/iZip/UnRarX for Mac

  • 7-Zip/PeaZip for Linux

本书代码包也托管在 GitHub 上,网址为github.com/PacktPublishing/Learn-Charts.js。如果代码有更新,它将在现有的 GitHub 仓库中更新。

我们还有其他来自我们丰富的图书和视频目录的代码包,可在github.com/PacktPublishing/上找到。查看它们吧!

使用的约定

本书使用了多种文本约定。

CodeInText:表示文本中的代码词汇、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称。以下是一个示例:“这设置了在fill()命令中使用的颜色。”

代码块设置如下:

const chartObj = {…}; // the chart data is here
const context = canvas.getContext("2d");
new Chart(context, chartObj); // this will display the chart in the canvas

任何命令行输入或输出都应如下编写:

npm install chart.js --save
bower install chart.js --save

粗体:表示新术语、重要词汇或屏幕上看到的词汇。例如,菜单或对话框中的文字会像这样显示。以下是一个示例:“您只需在“资源”选项卡中添加 Chart.js CDN。”

警告或重要注意事项如下所示。

小贴士和技巧看起来像这样。

联系我们

我们欢迎读者的反馈。

一般反馈:如果您对本书的任何方面有疑问,请在邮件主题中提及书名,并电子邮件我们至 customercare@packtpub.com

勘误:尽管我们已经尽最大努力确保内容的准确性,但错误仍然可能发生。如果您在这本书中发现了错误,我们将不胜感激,如果您能向我们报告,我们将不胜感激。请访问 www.packt.com/submit-errata,选择您的书籍,点击勘误提交表单链接,并输入详细信息。

盗版:如果您在互联网上以任何形式遇到我们作品的非法副本,我们将不胜感激,如果您能提供位置地址或网站名称,我们将不胜感激。请通过 copyright@packt.com 联系我们,并提供材料的链接。

如果您有兴趣成为作者:如果您在某个领域有专业知识,并且您有兴趣撰写或为书籍做出贡献,请访问 authors.packtpub.com.

评论

请留下评论。一旦您阅读并使用了这本书,为什么不在这本书购买的网站上留下评论呢?潜在读者可以看到并使用您的客观意见来做出购买决定,Packt 的我们能够了解您对我们产品的看法,而我们的作者可以查看他们对书籍的反馈。谢谢!

如需更多关于 Packt 的信息,请访问 packt.com.

第一章:简介

这是一本关于使用 JavaScript 和 Chart.js 进行数据可视化的书籍,Chart.js 是最受欢迎的数据可视化库之一,也是使用起来最简单的库之一。Chart.js 提供了现成的交互式可视化工具,您只需进行最少的编码即可使用。在将您的数据加载到标准的 JavaScript 数组后,您可以使用简单的基于对象的声明性结构添加样式和其他配置。Chart.js 会自动缩放您的数据,生成刻度和网格线,创建交互式工具提示,并适应可用空间,使您的图表自动响应。这是开始创建 Web 数据可视化的一种极好方式。

Chart.js 是免费的、开源的,并由 GitHub 上活跃的开发者社区维护。作为一个数据可视化库,它在 GitHub 上以星标数量排名第二。排名第一的是 D3.js,这是一个更大、更复杂的库,学习曲线陡峭。您仍然需要了解 JavaScript、HTML 和 CSS 才能使用 Chart.js,但您不必成为专家。除了 JavaScript 之外,它还基于其他 Web 标准,如 DOM、CSS 和 Canvas。图表自动在 Canvas 中渲染,并控制所有 canvas 配置、调整大小和像素比。您需要了解的关于 Canvas 的唯一事情是如何在您的页面上包含一个 <canvas> 标签并获取上下文,但 Canvas 知识对于高级图表可能很有用。

本书假设您已经了解一些 JavaScript、HTML 和 CSS。下一章提供了对特定主题和您在学习 Chart.js 时可能遇到的主要技术的快速回顾。如果您愿意,可以跳过它,直接跳到第三章,Chart.js 快速入门。本章简要介绍了数据可视化和数据可视化框架,并对本书的其余部分进行了概述。

本章将涵盖以下主题:

  • 数据可视化

  • 为什么使用数据可视化库?

  • 如何使用本书

数据可视化

视觉表示为数据提供了上下文,刺激了观众的头脑,揭示了通常从表格数据中不明显的信息。通常,使用视觉元素捕获数据的实质性内容要自然得多。图表和地图是讲述数据故事的有力、清晰和有效手段。它们可以在小空间中打包大量信息,并使比较数据、提供见解和揭示隐藏在数字中的趋势、关系、因果关系和其他模式变得更容易。

图表通过吸引观众的注意力来揭示和强调数据。它们可以将复杂的数据集简化,以促进发现和理解,帮助观众在不同的上下文中分析和推理数据。但它们也可能夸大、误导,甚至撒谎。一个视觉上吸引人的图表很重要,但设计师应该发现如何在形式和功能之间达到平衡。

数据可视化既是艺术又是科学。图表不需要解释一切。它不总是需要精确。它可能针对特定的受众,这应该提供理解和解码所需的上下文。

图表类型

数据可视化通常指的是定量信息的视觉展示,如统计图表、图表、数据地图等。但它可以包括任何类型的数据视觉表示,例如数学图形、路径网络(地铁系统、道路、电子电路图)、词云、音乐和声音表示、时间线、地理信息系统、化学和原子图,或任何其他使用视觉符号编码数据的方式。

如果你使用像 D3.js 这样的数据驱动库,你可以创建任何类型的可视化。带有预配置格式的图表库,如 Chart.js,功能更有限,但使用起来更容易。

Chart.js 支持八种基本图表类型:

  • 条形图(水平和垂直)

  • 线/面积(包括堆叠)

  • 雷达图(径向线)

  • 极坐标面积图(径向条形图)

  • 散点图

  • 气泡图

  • 饼图

  • 环形图

它不支持网络图、树或地理地图,但你可以使用 Chart.js 创建与其他图形共享数据的可视化。在第四章,创建图表中,我们将使用 Chart.js 创建一个气泡图,表示世界各地的城市人口,并将它们绘制在地图上。

选择图表

选择图表需要了解你的数据。图表是一种旨在揭示信息的沟通手段,所以主要问题是:你想要展示什么?一旦你回答了这个问题,你应该分析你的数据,并发现你有什么类型的数据。在可视化中使用的数据值通常可以分为以下三种类型之一:

  • 定量:可以测量或计数的值(数字、长度、面积、角度)

  • 序数:一个值可以被排序或比较(颜色饱和度、面积、角度、长度、单词)

  • 名义:一个类别(一个名称)

你的图表的目的是什么?你是想揭示关系、趋势还是因果关系?你希望强调哪种关系?你的变量与时间或空间相关吗?

可视化可以组织成类别,这有助于选择所需的图表类型。大多数图表和地图都可以归入以下类别之一:

  • 时间序列(在一段时间内绘制单个变量的图表)。例如,展示趋势的折线图。

  • 时间/线性(按时间线排列的分类)。例如,一系列事件。

  • 空间/平面/体积(在空间地图上分布的分类)。例如,带有地理地图上分布数据的人口图色块图

  • 比较(在单一时期内,与数量相关的类别被比较和排序)。例如,比较值的条形图。

  • 部分与整体(分类子集作为整体的比例)。例如,以百分比表示切片的饼图。

  • 相关性(比较两个或多个变量)。例如,比较两个变量的散点图,或比较三个变量的气泡图。

在他的经典著作《定量信息的视觉展示》中,爱德华·图费(Edward Tufte)定义了一些可以用来衡量可视化质量与完整性的方面。它们如下:

  • 数据-墨水比率:用于显示数据的墨水(或像素)量

  • 图表垃圾:与显示的数据无关的视觉垃圾(并且经常阻碍数据展示)

  • 谎言因子:衡量可视化完整性的数字;例如,通过不精确地表示比例和长度而撒谎的图表

通过从图表中移除不必要的线条和标签等图表垃圾(chart-junk),可以提高数据-墨水比率。有时线条对于上下文很重要,但在交互式网络可视化中,你可以非常简约。你总是可以通过工具提示或其他交互式资源提供需求的详细信息。

人类对图形的感知方式深刻地影响着沟通,可能会因为视觉错觉而被改善或扭曲。没有谎言因子为零的图表,但适当的选择可以显著提高它。一个糟糕的选择会增加谎言因子,并可能导致观众产生错误的感知。

位置和长度最适合表示定量信息。接下来是方向和角度,然后是面积、体积、曲率,最后是阴影、饱和度和颜色。由于长度和位置比角度和面积更容易感知和比较,因此条形图中的数据比饼图中的数据感知得更准确。考虑以下比较大陆面积的饼图:

比较大陆面积的饼图

现在看看以条形图表示的完全相同的数据:

比较大陆面积的条形图

哪一个更清晰?虽然饼图适合显示比例,但角度的比较要困难得多。长度的差异更容易比较,在这种情况下条形图更好。但使用饼图来比较两个值作为整体的一部分可能更清晰,例如,一个大陆的面积与剩余面积的比较,以揭示它相对于整个地球所占的土地比例。

要同时显示比例和长度,你可能想尝试一个单一的堆积条形图,但它仍然比简单的条形图差,因为堆积条不容易像并排放置时那样比较:

比较大陆面积的堆积条形图

当然,你可以出于其他原因选择图表,但了解每种情况下你失去和获得的是什么很重要。分析和探索性可视化需要高度精确,但如果你需要吸引观众的注意力,可能愿意以牺牲一些精确度为代价使用更具吸引力的可视化。

基于网络的可视化

当爱德华·图费(Edward Tufte)撰写他的关于数据可视化的经典书籍时,大部分内容都是针对印刷媒体的。今天,你可以创建使用实时更新的数据的可视化,具有允许用户交互的动态界面。基于网络的视觉化的设计指南是不同的。它们可以并且应该是动态和交互式的。交互式图表可以通过隐藏细节来包含更多信息,这些细节可以在用户需要时请求。图表有时被渲染为概览或 Sparklines,只有通过缩放和刷选等策略才能揭示细节。

为什么使用数据可视化库?

实际上,你不需要任何编码就能为网页创建出色的交互式可视化。你总是可以使用图表服务。有很多这样的服务;有些是付费的,有些是免费的。它们提供配置界面和数据转换工具,允许你创建各种美丽的图表。流行的服务包括Google ChartsTableauInfogramPlotly。如果它们完全满足你的数据可视化需求,你实际上并不需要这本书。

但如果你知道 HTML、CSS 和 JavaScript,你就不需要使用这些平台。网络标准提供了你创建任何复杂图形所需的所有工具,没有任何平台或计划的限制。使用网络标准还便于将你的可视化与网络应用程序、前端框架(如ReactAngularVue)以及后端网络服务(如 RESTful 服务提供的资源和数据)集成。

但你真的需要使用像 Chart.js 这样的库吗?难道你不能只使用标准的 JavaScript、HTML、CSS 和 Canvas 吗?

为网页创建数据可视化

你实际上并不需要任何库或框架来在网页上创建和显示交互式和动画数据可视化。标准的 HTML、CSS 和 JavaScript 已经为你做到了这一点。基本的 HTML 提供了可以通过 CSS 进行样式的结构元素,可以用来显示简单的图形元素,例如彩色矩形。这意味着你只需在 CSS 中应用不同的宽度到div元素,就可以创建一个基本的静态柱状图。

看看下面的 HTML 和 CSS 代码:

<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <style>
        #data {
            width: 600px;
            border: solid black 1px;
            padding: 10px;
        }
        .label {
            display: inline-block;
            width: 60px;
        }
        .bar {
            display: inline-block;
            background: red;
            margin: 2px 0;
            text-align: right;
            padding-right: 5px;
            color: white;
        }
        #data .bar1 { width: 500px; }
        #data .bar2 { width: 200px; }
        #data .bar3 { width: 300px; }

    </style>
</head>
<body>

<h1>Bar chart</h1>
<div id="data">
    <div><span class="label">Item 1</span><span class="bar bar1">50</span></div>
    <div><span class="label">Item 2</span><span class="bar bar2">20</span></div>
    <div><span class="label">Item 3</span><span class="bar bar3">30</span></div>
</div>

</body>
</html>

如果你在一个网页浏览器中运行它,你会得到以下图表:

仅使用 HTML 和 CSS 创建的简单静态柱状图

当然,这不是绘制条形图的最佳方式。数字在 HTML 中插入得有些随意,长度由 CSS 的宽度属性定义。你必须自己计算比例。很容易出错并显示错误的数据。这只是为了说明所有图形工具已经存在于 HTML 和 CSS 中。我们需要的只是一个为我们生成代码的库。

由于文档对象模型DOM)允许脚本语言访问和更改元素的风格属性,宽度可以在 JavaScript 中指定,甚至可以响应事件或随时间变化,从而允许创建具有动画的交互式图形。将以下 JavaScript 函数添加到前面的图表中,你可以在用户使用标准 DOM 命令点击图表时更改值、长度和颜色:

 <div id="data" onclick="changeData()">...</div>
 <script>
     function changeData() {
         let chart = document.getElementById("data");
         let elements = chart.children;
         let values = [5, 35, 15];
         for(let i = 0; i < values.length; i++) {
             let labelElement = elements[i].children[1].innerText = 
             values[i];
             elements[i].children[1].style.background = "blue";
             elements[i].children[1].style.width = values[i] * 10 + "px";
         }
     }
 </script>

以下截图显示了在点击后与前面相同的图表,具有新的颜色和值:

图片

使用标准 DOM、CSS、HTML 和 JavaScript 创建的交互式条形图

但你不必使用div和 CSS 来绘制条形图。自从 HTML5 以来,你也可以使用 Canvas API——这是一个完整的图形库,可以用来创建任何类型的图形,而不仅仅是矩形。以下示例仅使用标准 HTML 和没有额外库就显示了与饼图相同的数据:

<canvas id="chart" height="200" width="400"></canvas>

<script>
    const canvas = document.getElementById('chart');
    const ctx = canvas.getContext('2d');
    const rad = Math.min(canvas.height, canvas.width) / 2;

    const data = [100, 100, 100];
    const comp = n => Math.floor(Math.random() * 255);
    const colors = function() {
        return "rgba("+comp()+","+comp()+","+comp()+",0.5)";
    };

    let angle  = 0.0;
    let total  = data.reduce((a, b) => a + b, 0);

    for (var i = 0; i < data.length; i++) {
        ctx.fillStyle = colors();
        ctx.strokeStyle = 'white';
        ctx.lineWidth = 4;
        ctx.beginPath();
        ctx.moveTo(rad, rad);
        ctx.arc(rad, rad, rad, angle, angle + 
               (Math.PI * 2 * (data[i]/total)), false);
        ctx.lineTo(rad, rad);
        ctx.fill();
        ctx.stroke();
        angle += Math.PI * 2 * (data[i] / total);
    }
</script>

你可以从本章的 GitHub 仓库下载所有这些代码示例。详情请见最后一节。结果如下所示:

图片

使用 HTML Canvas 创建的简单饼图

在 Chart.js 中,即使不写一行 Canvas 代码,你也能制作出更漂亮的饼图,你也不必担心计算弧度或调整比例以适应页面。你还可以用比之前更少的代码获得免费的工具提示。Chart.js 可能无法满足你所有的数据可视化需求,但它确实是一个很好的开始方式。然后你可以应用你的 HTML 和 JavaScript 知识,并通过插件扩展它,与其它图表解决方案集成,或者迁移到一个更大且不受限制的库,如 D3.js。

如何使用本书

本书旨在作为创建数据可视化的 Chart.js 实用手册。它不涵盖 Chart.js 的所有方面,但涵盖了创建可视化时可能使用的大多数功能。涵盖了所有八个 Chart.js 图表,探讨了使用从公共仓库获取的外部数据的不同配置和应用。

每一章都涵盖了基本概念。每个概念都总是用一个简单的代码示例来展示,但每个章节也展示了可能需要额外 JavaScript 编程的更复杂示例,并包括解决现实世界问题的解决方案,例如下载、解析和过滤数据文件以将其转换为 Chart.js 可用的格式。

书中使用了代码列表,但大多数时候,它们专注于一个特定的功能,只展示完整代码的一部分。但你可以下载完整的列表。书中使用的所有代码示例都可以从位于:github.com/PacktPublishing/Learn-charts.js 的公共 GitHub 仓库中获取。

仓库中为每个章节都有一个文件夹,命名为 Chapter01Chapter02 等。每个代码列表和每个由代码生成的图像都包含对每个章节对应文件夹中文件的相对引用。你可以在学习时尝试所有代码示例。你也可以以任何你喜欢的方式使用它,因为它是免费和开源的。

下面是每个章节的简要总结:

第二章,技术基础,涵盖了技术基础、数据格式,并探讨了加载和解析文件的一些技术。它提供了 Chart.js 所用主题的一般背景,例如 JavaScript、CSS、DOM 和 Canvas。如果你愿意,可以跳过这些部分。

第三章,Chart.js 快速入门,包括快速入门和几个 Chart.js 功能的概述。它展示了如何设置网页以使用 Chart.js 以及如何创建你的第一个图表。你将了解 Chart.js 提供的所有内容。它还介绍了条形图类型(垂直和水平)。

第四章,创建图表,涵盖了所有其他可用的图表类型:折线/面积图、雷达图、极坐标面积图、饼图、环形图、散点图和气泡图。它还展示了如何从公共数据门户加载和解析外部 CSV 和 JSON 数据,并使用它们创建现实世界的可视化。

第五章,刻度和网格配置,专注于配置所有图表的刻度、轴和网格线。你将学习如何使用径向网格和笛卡尔网格与线性、对数、分类和时间轴一起使用。

第六章,配置样式和交互性,处理了配置几个属性的问题,对于这些属性,Chart.js 已经提供了默认值:字体、标题和标签。它还探讨了几个实用的标签插件和配色方案,这对于可访问图表非常重要。本章还展示了如何配置过渡、动画和交互。

第七章,高级 Chart.js,涵盖了您不太可能经常使用的某些高级功能,因为 Chart.js 已经提供了良好的默认设置。这些包括工具提示配置、创建自定义图例、混合图表、如何在单个页面上显示多个图表、在 Canvas 上叠加 Chart.js 以及创建插件。

我认为这本书涵盖了 Chart.js 中最重要的主题。它省略了一些高级编程主题、几个插件以及与前端框架的集成,这些内容也没有涉及。

我与 Chart.js 没有任何关联,这本书也不代替官方的 Chart.js 文档,这是该主题的终极参考指南。该文档由社区维护,免费提供在 www.chartjs.org/docs/latest。还有许多探索其主要功能的示例,这些示例也是文档的一部分,可在 www.chartjs.org/samples/latest 找到。

在撰写这本书时,我尽力提供尽可能准确的信息。所有代码列表都经过测试,并做出了额外努力以确保所有代码示例在书中得到适当引用,并且按预期工作。本书基于 Chart.js 版本 2.7.3。我预计示例应该继续与任何 2.x 版本兼容,但如果您使用的是较新版本,某些代码可能无法按预期工作。

Chart.js 社区在 GitHubStackOverflow 上非常活跃。如果您对 Chart.js 有任何疑问,您可以在 stackoverflow.com 上提交问题,您可能会在几小时或更短的时间内得到答案。

我希望您喜欢这本书,并在学习 Chart.js 时像我一样享受乐趣。

摘要

本章提供了数据可视化主题的快速介绍,并介绍了 Chart.js,这是一个 JavaScript 库,您将学习如何创建用于网络的响应式交互式可视化。我们还展示了标准网络技术如何提供您创建网络图表所需的一切,以及如何使用像 Chart.js 这样的数据可视化库带来益处。

在下一章中,我们将介绍 Chart.js 所使用的某些标准技术的基础知识——数据格式和数据操作技术——但如果您想立即开始使用 Chart.js,您可以跳过它,直接跳到 第三章,Chart.js 快速入门

参考文献

书籍和网站

  • 爱德华·R·图费。《定量信息的视觉展示》。图形出版社,1997 年

  • 伊莎贝尔·梅雷莱斯。《信息设计》。洛克波特出版社,2013 年

  • 史蒂芬·菲。数据可视化:过去、现在和未来。感知边缘,2007 年

  • 大卫·卡尼曼。《快速与慢思考》。法拉尔、斯特劳斯和吉鲁出版社,2011 年

  • Ben Bederson 和 Ben Schneiderman. 信息可视化的艺术. 2003

第二章:技术基础

本书假设你已经掌握了 HTML、CSS 和 JavaScript 的基本知识,这些是使用 Chart.js 创建可视化的必备工具。本书中的所有示例都是用 JavaScript ES2015 或 ES6 编写的。本章的一个目标是对这些技术的核心主题进行回顾。这包括与字符串、对象和数组操作相关的 JavaScript 主题,HTML 文档对象模型(DOM),基本的 JQuery,CSS 选择器,以及 HTML 画布。当然,如果你已经对这些技术感到熟悉,你可以跳过这些部分。

本章还描述了在可视化中常用的数据格式,如 CSV、XML 和 JSON,以及如何在你的 Web 页面中加载、解析和使用这些格式的外部数据文件。你还将学习如何设置一个小型测试 Web 服务器来运行加载外部资源的文件。

最后部分包含了一些关于如何获取和准备数据以供你的可视化使用、如何将 HTML 数据转换为标准格式,以及如何从 HTML 页面中提取所需信息的技巧。

在本章中,你将学习以下内容:

  • Chart.js 必需的 JavaScript

  • 其他技术:DOM、CSS、JQuery 和 Canvas

  • 数据格式

  • 如何加载和解析外部数据文件

  • 如何提取和转换数据

Chart.js 必需的 JavaScript

客户端应用程序,如交互式 Web 图形,依赖于浏览器支持。本书假设你的受众使用支持 HTML5 Canvas 和 ES2015(包括所有现代浏览器)的浏览器。所有示例都使用 ES2015 语法,包括constlet代替var,在适当的地方使用箭头函数,使用扩展运算符、映射、集合和承诺。外部文件使用 Fetch API 加载,该 API 最近才得到支持,但如果你需要,可以轻松切换到 JQuery。

虽然使用 Chart.js 创建可视化主要是一个声明性过程,但它仍然是一个 JavaScript 库,需要基本的 JavaScript 知识。要创建一个简单的图表,你需要知道如何声明常量和变量,执行基本的数学布尔字符串和属性操作,调用和创建函数,操作对象和数组,以及实例化 Chart.js 对象。典型的图表还需要足够的知识来编程控制结构,编写回调函数,排序和过滤数据集,生成随机数,以及加载外部文件。本节是对你使用 Chart.js 所需的主要 ES2015 主题的快速回顾。

浏览器工具

你不需要一个完整的客户端模块化 Node 开发环境来使用 Chart.js 创建可视化,但你仍然需要一个良好的调试器。每个浏览器都自带开发工具,允许你导航静态页面结构和生成的 DOM 元素,以及一个控制台,你可以实时与 JavaScript 引擎使用的数据进行交互。

最重要的工具是JavaScript 控制台,在这里你会看到任何错误信息。当你期望看到其他内容却得到一个空白页面,并且不知道为什么你的代码没有按预期工作,这种情况非常常见。有时,可能只是你忘记了一个逗号,或者网络连接中断导致某些文件没有加载。如果你在运行页面时打开了 JavaScript 控制台,它会立即告诉你发生了什么。使用带有行号的编辑器也是一个好主意,因为大多数错误信息都会告诉我们问题发生的位置:

图片

使用 JavaScript 控制台调试 JavaScript

你可以将开发者工具作为浏览器中的一个框架或作为单独的窗口打开。以下是最受欢迎的三种浏览器最新版本中 JavaScript 控制台的菜单路径:

  • Chrome: 查看 | 开发者 | JavaScript 控制台

  • Firefox: 工具 | 网页开发者 | 网页控制台

  • Safari: 开发 | 显示错误控制台

本节中的大多数代码片段和示例都可以通过在 JavaScript 控制台中输入它们来测试。这是一种学习 JavaScript 的好方法。它还会访问通过<script>标签加载的任何 JavaScript 库文件的功能,以及<script></script>块中声明的任何全局变量。

JavaScript 类型和变量

JavaScript 不是一种强类型语言,因为类型没有声明,变量可以接收不同的类型,但数据值确实有类型。主要类型有NumberStringBooleanArrayObjectFunction。前三种是标量类型,后三种也是对象。如果值具有不同的类型,它们在同一个表达式中会被以不同的方式处理。例如,在一个表达式如a = b + c中,如果bc是数字(它们将被相加),或者其中一个是一个字符串(它们将被连接),那么a的值将会不同。

值可以进行比较,如果比较是严格的(例如,使用===而不是==),那么它们的类型就很重要。但是,依赖于这样的转换可能会令人困惑(0""nullNaNundefined都被认为是false,但false字符串转换为true,因为它不是一个空字符串)。

在 ES5 JavaScript 中,var是声明变量的唯一关键字。它忽略了块作用域,并且会被提升到函数的顶部。自从 ES6(ES2015)以来,引入了两个新的关键字:constlet。它们都是块作用域的,并且在使用之前需要赋值(var默认为undefined)。使用const的声明是常量,不能重新赋值。通常认为,尽可能使用const是好的做法,只有在确实需要重新定义变量时才使用let

图表中使用的数据结构

用作可视化数据源的数据通常以某种结构组织。最常见的数据结构可能是列表(数组)和表格(映射),存储在某种标准数据格式中。当使用外部数据源时,你通常需要对其进行清理,删除不必要的值,简化其结构,应用边界等。之后,你可以解析它,并将其最终存储在本地 JavaScript 数组或 JavaScript 对象中,以便图表使用。

一旦你的数据存储在 JavaScript 数据结构中,你可以通过在存储的值上应用数学运算来进一步转换它。你可以更改结构,创建新字段,合并和删除数据。典型操作包括将新值推送到数据集中,切片或拆分数组,创建子集,转换数据等。JavaScript 提供了许多原生操作,使修改数组和对象变得更容易。你还可以使用 JQuery 等库。

Arrays

你将使用的主要数据结构来存储一维数据是 JavaScript 数组。它是 Chart.js 中大多数图表类型的主要数据集格式。制作一个简单的柱状图只需要一个值数组。你可以通过在括号内声明一个项目列表来创建一个数组,或者如果你想从一个空数组开始,只需一对开闭括号:

const colors = ["red", "blue", "green"];
const geocoords = [27.2345, 34.9937];
const numbers = [1,2,3,4,5,6];
const empty = [];

你可以使用数组索引访问数组的项,索引从零开始计数:

const blue = colors[1];
const latitude = geocoords[0];

每个数组都有一个长度属性,它返回元素的个数。使用数组索引进行迭代非常有用:

for(let i = 0; i < colors.length; i++) {
    console.log(colors[i]);
}

你还可以使用of运算符(在 ES2015 中引入)遍历数组的元素,当你不需要索引时:

for(let color of colors) {
    console.log(color);
}

你可以使用forEach()方法,它为每个元素运行一个函数,并允许在函数内部访问索引、项和数组:

colors.forEach(function(i, color, colors) {
    console.log((i+1) + ": " + color);
}

在 JavaScript 中,多维数组是以数组数组的格式创建的:

const points = [[200,300], [150,100], [100,300]];

你可以这样检索单个项:

const firstPoint = points[0];
const middleX = points[1][0];

JavaScript 提供了许多方法来提取和插入数组中的数据。通常建议尽可能使用这些方法。以下表格列出了你可以用于数组的有用方法。一些方法会修改数组;其他方法返回新的数组和其他类型。提供的示例使用之前声明的colorsnumbers数组。你可以使用浏览器中的 JavaScript 控制台尝试它们:

方法 描述 示例
push(item) 修改数组,在末尾添加一个项。
colors.push("yellow"); 
/*["red", "blue", "green", 
   "yellow"];*/

|

pop() 修改数组,移除并返回最后一个项。
const green = colors.pop(); 
// ["red", "blue"];

|

unshift(item) 修改数组,在开头插入一个项。
colors.unshift("yellow");
/*["yellow", "red", "blue", 
   "green"];*/

|

shift() 修改数组,移除并返回第一个项。
const red = colors.shift();
// ["blue", "green"];

|

splice(p,n,i) 从位置p开始修改数组。可用于删除、插入或替换项。
const s = numbers.splice(2,3);
 // s = [3,4,5]
 // numbers = [1,2,6]

|

reverse() 修改数组,反转其顺序。
numbers.reverse(); 
// [6,5,4,3,2,1]

|

sort() 修改数组,按字符串顺序(如果没有参数)或按比较函数排序。
numbers.sort((a,b) => b – a);
// numbers = [6,5,4,3,2,1]

|

slice(b,e) 返回从 be 的数组的一个浅拷贝。
const mid = numbers.slice(2,4)
 // mid = [3,4]

|

filter(callback) 返回一个新数组,其中的元素通过函数实现的测试。
const even = numbers.filter(n => n%2==0);
// [2,4,6]

|

find(function) 返回满足测试函数的第一个元素。
const two = numbers.find(n => n%2==0);
// 2

|

indexOf(item) 返回数组中项目首次出现的位置索引。
const n = numbers.indexOf(3); 
// 4

|

includes(item) 如果数组在其条目中包含项目,则返回 true
const n = numbers.includes(3); 
// true

|

lastIndexOf(item) 返回数组中项目最后一次出现的位置索引。
const n = colors.lastIndexOf("blue"); 
// 1

|

concat(other) 返回一个新数组,它将当前数组与另一个数组合并。
const eight = numbers.concat([7,8]);
// [1,2,3,4,5,6,7,8]

|

join()``join(delim) 返回数组中元素的逗号分隔字符串(可以使用可选的分隔符)。
const csv = numbers.join();
// "1,2,3,4,5,6"
const conc = numbers.join("");
// "123456"

|

map(function) 返回一个新数组,其中的每个元素都通过函数修改。
const squares = numbers.map(n => n*n);
// [1,4,9,16,25,36]

|

reduce(function) 返回使用数组中的值进行的累积操作的结果。
const sum =
  numbers.reduce((a, n) => a + n);

|

forEach(function) 对数组中的每个元素执行提供的函数一次。
const squares = [];
numbers.forEach(n => squares.push(n*n)
// squares = [1,4,9,16,26,36]

|

JavaScript 数组操作函数

除了数组之外,ES2015 还引入了两种新的数据结构:Map,一个具有键值对的关联数组,比简单对象更容易使用,以及 Set,它不允许重复值。两者都可以转换为数组。

字符串

在 JavaScript 中,字符串是原始类型,可以使用单引号或双引号创建。它们之间没有区别,这只是风格问题。ES2015 引入了两个新的字符串特性:模板字符串多行字符串

多行字符串可以通过在每行末尾添加反斜杠来创建:

const line = "Multiline strings can be \
reated adding a backslash \
at the end of each line";

模板字符串是通过反引号创建的字符串。它们允许在 ${} 插值占位符内包含 JavaScript 表达式。结果作为单个字符串连接:

const template = `The square root of 2 is ${Math.sqrt(2)}`;

如果需要在字符串中使用特殊字符,例如双引号字符串中的双引号或反斜杠,需要在它前面加上反斜杠:

const s = "This is a backslash \\ and this is a double quote \"";

有几种字符串操作方法。它们都返回新的字符串或其他类型。没有方法修改原始字符串:

方法 描述 示例
startsWith(s) 如果字符串以作为参数传递的字符串开头,则返回 true
const s = "This is a test string"
const r = s.startsWith("This");    
// true

|

endsWith(s) 如果字符串以作为参数传递的字符串结尾,则返回 true
const s = "This is a test string"
const r = s.endsWith("This"); 
// false

|

substring(s,e) 返回从 start(包含)到 end 索引(不包含)之间的子字符串。
const k = "Aardvark"
const ardva = k.substring(1,6);

|

split(regx)``split(delim) 通过分隔符字符或正则表达式分割字符串,并返回一个数组。
const result = s.split(" ");
// result =
//["This","is","a","test","string"]

|

indexOf() 返回子字符串首次出现的位置索引。
const k = "Aardvark"
const i = k.indexOf("ar"); // i = 1

|

lastIndexOf() 返回子字符串最后一次出现的位置索引。
const k = "Aardvark"
const i = k.lastIndexOf("ar");
// i = 5

|

charAt(i) 返回索引 *i* 处的 char。也支持为 ‘string'[i]
const k = "Aardvark"
const v = k.charAt(4);

|

trim() 从字符串的两端删除空白字符。
const text = "   data   "
const r = data.trim(); 
// r = "data"

|

match(regx) 返回一个数组,该数组是正则表达式与字符串匹配的结果。
const k = "Aardvark"
const v = k.match(/[a-f]/g);
// v = ["a", "d", "a"]

|

replace(regx,r)``replace(s,t) 返回一个新的字符串,该字符串将应用于字符串的正则表达式匹配替换为替换文本,或将源字符串的所有出现替换为目标字符串。
const k = "Aardvark"
const a = p.replace(/a/gi, 'u')
// a = "uurdvurk"
const b = p.replace('ardv', 'ntib')
// b = "Antibark"

|

JavaScript 字符串操作函数

函数

函数通常使用 JavaScript 中的 function 关键字创建,使用以下形式之一:

function f() {
    console.log('function1', this);
}
const g = function(name) {
    console.log('function ' + name, this);
}
f(); // calls f
g('test'); // calls g() with a parameter

this 关键字指向拥有函数的对象。如果此代码在浏览器中运行,并且这是一个在 <script> 块中创建的顶级函数,则所有者是全球的 window 对象。通过此访问的任何属性都指向该对象。

函数可以放置在对象的范围内,作为方法使用。以下代码中的 this 引用指向 obj 对象,可以访问 this.athis.b

const obj = {a: 5, b: 6}
obj.method = function() {
    console.log('method', this)
}
object.method()

箭头函数是在 ES2015 中引入的。它们更加紧凑,可以使代码更简洁,但 this 的作用域不再由对象保留。在以下代码中,它引用全局的 window 对象。使用 this.athis.b 的代码将不会在对象中找到任何数据,并返回 undefined

obj.arrow = () => console.log('arrow', this)
object.arrow()

你可以在 Chart.js 回调中使用箭头函数,但如果你需要访问图表实例,通常使用 this,你应该使用普通函数而不是箭头函数。

对象

对象是无序的数据集合。对象中的值存储为键值对。你可以通过在花括号内声明逗号分隔的 键:值 对来创建对象,或者如果你想要从空对象开始,可以简单地使用一对开闭花括号:

const color = {name: "red", code: ff0000};
const empty = {};

对象可以包含其他对象和数组,这些数组也可以包含对象。它们还可以包含函数,这些函数可以访问局部属性,并作为方法使用:

const city = {name: "Sao Paulo",
              location: {latitude: 23.555, longitude: 46.63},
              airports: ["SAO","CGH","GRU","VCP"]};
const circle = {
    x: 200,
    y: 100,
    r: 50,
    area: function() {
        return this.r * this.r * 3.14;
    }
}

简单图表通常使用的典型数据集通常由一个对象数组组成:

var array2 = [
    {continent: "Asia", areakm2: 43820000},
    {continent: "Europe", areakm2: 10180000},
    {continent: "Africa", areakm2: 30370000},
    {continent: "South America", areakm2: 17840000},
    {continent: "Oceania", areakm2: 9008500},
    {continent: "North America", areakm2=24490000}
];

你可以使用点操作符或包含键作为字符串的方括号来访问对象的属性。你可以使用点操作符运行其方法:

const latitude = city.location.latitude;
const oceania = array2[4].continent;
const code = color["code"];
circle.r = 100;
const area = circle.area();

你也可以遍历对象的属性:

for(let key in color) {
     console.log(key + ": " + color[key]);
}

可以向对象添加属性和函数。在全局范围内声明一个空对象以声明代码很常见,以便在其他上下文中添加数据:

const map = {};
function getCoords(coords) {
    map.latitude = coords.lat;
    map.longitude = coords.lng;
}

对象也可以通过构造函数创建。你可以使用以下方式创建包含当前日期/时间的对象:

const now = new Date();

创建一个 Chart.js 实例需要使用至少两个参数的构造函数。第二个参数是一个对象,包含两个属性,一个字符串和另一个对象:

const chart =
    new Chart("bar-chart ",{type:"bar", data:{labels:[],datasets:[]}});

JSON 是基于 JavaScript 对象的数据格式。它与 JavaScript 对象具有相同的结构,但属性键必须放在双引号内:

{"name": "Sao Paulo",
              "location": {"latitude": 23.555, "longitude": 46.63},
              "airports": ["SAO","CGH","GRU","VCP"]};

要在 JavaScript 中使用 JSON 字符串,你必须解析它。

其他技术

本节简要介绍了你应该了解的其他技术,包括它们的基本概念。它们包括 HTML DOM、JQuery、CSS 和 HTML Canvas。如果你已经了解并使用这些技术,可以浏览或跳过本节。接下来的几节还提供了可以从本章 GitHub 仓库下载的代码示例。

HTML 文档对象模型(DOM)

HTML 文档的结构通常用标签描述,但也可以使用带有 Document Object Model (DOM) 的 JavaScript 命令指定:一个语言中立的 API,它将 HTML 或 XML 文档表示为 。考虑以下 HTML 文档(Examples/example-1.html):

<html>
<body>
     <h1>Simple page</h1>
     <p>Simple paragraph</p>
     <div>
         <img src="img/pluto.jpg" width="100"/>
         <p>Click me!</p>
     </div>
 </body>
 </html>

本页构建了一个包含 HTML 元素和文本的相互连接的 节点 树。可以使用以下 JavaScript 命令获得相同的结果(Examples/example-2.html):

 const html = document.documentElement;

 const body = document.createElement("body");
 html.appendChild(body);

 const h1 = document.createElement("h1");
 const h1Text = document.createTextNode("Simple page");
 h1.appendChild(h1Text);
 body.appendChild(h1);

 const p = document.createElement("p");
 const pText = document.createTextNode("Simple paragraph");
 p.appendChild(pText);
 body.appendChild(p);

 const div = document.createElement("div");
 const divImg = document.createElement("img");
 divImg.setAttribute("src", "pluto.jpg");
 divImg.setAttribute("width", "100");
 div.appendChild(divImg);

 const divP = document.createElement("p");
 const divPText = document.createTextNode("Click me!");
 divP.appendChild(divPText);
 div.appendChild(divP);

 body.appendChild(div);

当然,编写标签要简单得多,但 JavaScript 给你提供了使结构和内容 动态化 的能力。使用 DOM 命令,你可以添加新元素,移动它们,删除它们,并更改它们的属性和文本内容。你还可以在 DOM 树中导航,选择或搜索特定元素或数据,并将样式和事件处理器绑定到元素上。

例如,如果你添加以下代码,每次点击图片时都会创建一个新的 <p> 元素,包含 “New line” 文本(Examples/example-3.html):

div.style.cursor = "pointer";
div.addEventListener("click", function() {
    const p = document.createElement("p");
    p.innerHTML = "New line";
    this.appendChild(p);
});

通常,你不会使用 DOM 编写整个文档,而只是控制动态部分的文档。通常,你会将静态部分编写为 HTML,并在必要时使用脚本(Examples/example-4.html):

 <html>
 <body>
 <h1>Simple page</h1>
 <p>Simple paragraph</p>
 <div id="my-section">
     <img src="img/pluto.jpg" width="100"/>
     <p>Click me!</p>
 </div>
 </body>

 <script>
     const div = document.getElementById("my-section");
     div.style.cursor = "pointer";
     div.addEventListener("click", function() {
         const p = document.createElement("p");
         p.innerHTML = "New line";
         this.appendChild(p);
     });
 </script>
 </html>

对于数据驱动的文档,你可以使用 DOM 脚本来将存储在数组或对象中的数据绑定到元素的属性,从而改变尺寸、颜色、文本内容和位置。大多数数据可视化库正是通过提供基于 DOM 的函数来实现这一点,使这项任务变得简单得多。

以下表格列出了最重要的 DOM 命令:

方法或属性 描述
createElement(tag) 创建一个元素(不连接到节点树)并返回其引用。
createTextNode(text) 创建一个文本节点(不连接到节点树)并返回其引用。
appendChild(element) 将传入参数的元素作为当前元素的子元素连接。
removeChild(element) 将子元素从当前元素中断开连接。
setAttribute(name, value) 使用传入的参数设置此元素的属性名称和值。
getElementById(id) 返回一个通过传入参数的 id 标识的元素。
getElementsByTagName(tag) 返回一个包含所有匹配传入参数标签名的元素的 nodelist(数组)。
addEventListener(e, func) 将事件处理程序附加到该元素。第一个参数是事件类型(例如,‘click', ‘key', 等等)第二个参数是处理函数。
documentElement 该属性引用文档根部的元素。对于 HTML 和 XHTML,它是<html>元素。
children 该属性返回一个包含此元素子元素的节点列表。
innerText 在 SVG 或 HTML 文档中,这个读写属性是一个创建文本节点并将其附加到元素的快捷方式。
innerHTML 在 HTML 文档中,这个读写属性是一个将整个 HTML 片段作为子元素附加的快捷方式。
style 在 SVG 或 HTML 文档中,该属性允许访问元素的 CSS 样式。您可以使用它动态地读取和修改样式。

HTML DOM 支持的属性和方法选择

层叠样式表

层叠样式表CSS)是一个 W3C 标准,它指定了 HTML 和 XML 元素在屏幕上的显示方式。它是一种声明性语言,其中视觉属性应用于标签选择器。您可以使用 CSS 将颜色、字体、边距、阴影和渐变等属性应用于一个或多个标签,在二维和三维中进行坐标变换,并设置过渡和动画的规则。CSS 属性和选择器也用于 JavaScript 库,如 JQuery 和 D3.js。

CSS 选择器是用于通过类型、类、ID、通配符、属性、上下文、状态和位置选择元素的表达式。选择表达式的结果可能由零个、一个或多个元素组成。JavaScript 库使用选择器来获取可以通过 DOM 编程方式操作的对象。可以从逗号分隔的选择表达式列表中形成结果集。元素也可以通过组合选择器从上下文中选择。以下表格列出了主要选择器及其一些示例:

选择器 语法 描述示例(在 CSS 中)
类型选择器 tagname 选择指定类型(标签名)的元素集合,例如 td, h1, prect { … } /* 所有 <rect> 标签 */
类选择器 .classname 选择属于指定类的元素集合,例如 .selected 和 p.copy
ID 选择器 #idname 选择具有指定id属性的元素,例如 #main 和 #chart
全局选择器 * 选择所有元素。
属性选择器 [attr] [attr=value](其他几种组合) 选择包含属性的元素。选择具有指定值的属性的元素。其他组合匹配属性值中的字符串。
后代组合器 ancestor selectedtag 选择嵌套在指定祖先元素内的元素(之间可能包含其他元素),例如 table td
子组合器 parent > selectedtag 选择直接位于指定父元素(selectedTag是父元素的子元素)下方的元素,例如 table >tbody >tr >td
一般兄弟组合器 preceding ~ selectedtag 选择出现在指定前驱之后(两者具有相同的父元素)的元素,例如 h1 ~p.last
相邻兄弟组合器 previous + selectedtag 选择出现在指定兄弟之后(两者具有相同的父元素)的元素,例如 h1 +p.first
伪类 tag:state 选择处于指定state的元素,例如 a:hover, p:last-child, td:nth-of-type(2), :not(x)
伪元素 tag::property 选择具有指定property的元素,很少使用。

CSS 选择器

大多数时候,您将使用最简单的选择器。ID、类和类型选择器是最常见的。最终,您可能会使用后代组合器或属性选择器。

以下代码使用简单选择器更改包含三个部分的未格式化页面的视觉外观。这些部分是堆叠在一起的。省略了 CSS 属性和其他部分,但您可以在完整的代码列表中看到它们(Examples/example-5-selectors.html):

<html lang="en">
<head>
    <style>
        h1 {…}
        .tab h1 {…}
        .tab p {…}
        .illustration {…}
        .tab {…}
        .tab .contents {…}
        .container {…}
        .tab:nth-child(2) h1 {…}
        .tab:nth-child(3) h1 {…}
    </style>
</head>

<body>
<h1>CSS and JQuery selectors</h1>

<div id="container">

    <div class="tab first" id="section1">
        <div class="contents">
            <img class="illustration" src="img/jupiter.jpg" />
            <p>…</p>
        </div>
        <h1>Tab 1: Jupiter</h1>
    </div>

    <div class="tab" id="section2">
        <div class="contents">
            <img class="illustration" src="img/saturn.jpg" />
            <p>…</p>
        </div>
        <h1>Tab 2: Saturn</h1>
    </div>

    <div class="tab" id="section3">
        <div class="contents">
            <img class="illustration" src="img/pluto.jpg" />
            <p>…</p>
        </div>
        <h1>Tab 3: Pluto</h1>
    </div>

</div>
</body>

</html>

结果如下:

仅使用 CSS 样式化的堆叠信息的 HTML 页面

JQuery 基础

JQuery 不是一个标准技术,但它是一个事实上的 Web 标准。它使用 CSS 选择器在任意 HTML 文件中定位元素,并提供与 DOM 相同的功能,但语法更简洁。要使用JQuery,您首先需要使用<script>标签将其库包含在您的 HTML 页面中。这可以通过 CDN URL 轻松完成:

<script src="img/jquery-3.3.1.min.js"></script>

此代码片段是一个使用JQuery执行与最后一个 DOM 示例中显示的完全相同操作的页面。结果是更容易理解(Examples/example-6.html):

<html>
<body>
<head>
    <style>
        #my-section {
            cursor: pointer;
        }
    </style>
</head>
<h1>Simple page</h1>
<p>Simple paragraph</p>
<div id="my-section">
    <img src="img/pluto.jpg" width="100"/>
    <p>Click me!</p>
</div>
</body>
<script src="img/jquery-3.3.1.min.js"></script>
<script>
    $("#my-section").on("click", function() {
        $(this).append("<p>New Line</p>");
    });
</script>
</html>

CSS 选择器用于 JavaScript 库,如JQuery,以应用动态样式和操作文档的结构和内容。主要的 JQuery(selector)函数,通常通过其别名$(selector)函数使用,是一个元素选择器,它接收一个 CSS 选择器表达式作为其参数:

const divSet = $("div");
const title1 = $("#section1 h1");

选择可以返回零个、一个或一个元素列表。您可以使用length属性测试选择的长度:

if($("table").length == 0) {
     console.log("there are no tables in this document")
 }

使用JQuery和 CSS 示例中显示的代码,我们可以通过选择器和JQuery函数使选项卡在点击时淡入和淡出(Examples/example-7-selectors.html):

<script src="img/jquery-3.3.1.min.js"></script>
<script>
    $(".tab").on("click", function() {
        $(".tab h1").css("color", "gray");
        $(".tab h1").css("background", "white");
        $(".tab h1").css("font-weight", "normal");
        $(".tab h1").css("z-index", "-1");
        $("#" + $(this).attr("id") + " h1").css("color", "black");
        $("#" + $(this).attr("id") + " h1").css("background", "whitesmoke");
        $("#" + $(this).attr("id") + " h1").css("font-weight", "bold");
        $("#" + $(this).attr("id") + " h1").css("z-index", "1");
        $(".tab .contents").fadeOut();
        $("#" + $(this).attr("id") + " .contents").fadeIn();
    });
    $("#section1").trigger("click");
</script>

HTML5 画布

使用 HTML 标签无法绘制圆形或渐变,但您可以使用 HTML Canvas:一个用于 2D 矢量图形的完整功能的 JavaScript 图形 API。您可以使用 Canvas 绘制任何您想要的,由于它是 JavaScript,您可以使其动画化并响应用件。

要使用 Canvas 绘制,您需要在您的页面上创建一个 <canvas> 元素。您可以使用纯 HTML 来做这件事:

<body>
    <canvas id="canvas" width="300" height="300"></canvas>
</body>

您还可以使用 HTML DOM 动态创建它:

const canvas = document.createElement("canvas");
canvas.setAttribute("width", 300);
canvas.setAttribute("height", 300);
document.body.appendChild(canvas);

您也可以使用 JQuery 创建它:

const canvas = $("<canvas/>",{id: "canvas"}).prop({width:300,height:300});

然后,您可以使用 DOM 引用:

const canvas = document.getElementById("canvas");
const canvas = $("#canvas");

或者,您可以使用 jQuery 引用:

const canvas = document.getElementById("canvas");
const canvas = $("#canvas");

一旦您有了画布对象,您就可以获取一个 2D 图形上下文并开始绘制:

const ctx    = canvas.getContext("2d");

实际上,Canvas API 主要由从图形上下文调用的方法和属性组成。在绘制之前,您设置诸如字体、填充颜色和描边颜色等属性:

ctx.fillStyle = "red";
ctx.strokeStyle = "rgba(255,127,0,0.7)";
ctx.lineWidth = 10;

然后填充或描边矩形和包含线条和曲线的任意路径。这些命令将在位置 50,50 绘制一个红色 50 x 50 像素的正方形,并带有 10 像素宽的黄色半透明边框:

ctx.fillRect(50,50,50,50);
ctx.strokeRect(50,50,50,50);

您可以在同一画布上绘制其他形状、文本和图像。上下文属性除非重新定义,否则不会改变。

您也可以使用路径命令来绘制。您需要从 ctx.beginPath() 开始路径,然后调用一系列命令将点移动到并绘制线条和曲线,完成后您可以关闭路径(如果它是闭合路径),并调用 fill() 和/或 stroke() 来使用当前样式绘制它。

以下代码绘制了一些形状、路径、阴影、渐变和文本:

 ctx.strokeStyle = "blue";
 ctx.lineWidth = 2;
 ctx.shadowBlur = 10;
 ctx.shadowColor = "green";
 ctx.shadowOffsetX = ctx.shadowOffsetY = 5;
 ctx.setLineDash([5,2,1,2]);
 ctx.beginPath();
 ctx.moveTo(150,200);
 ctx.lineTo(150,150);
 ctx.lineTo(100,150);
 ctx.bezierCurveTo(100,200,150,250,200,250);
 ctx.lineTo(200,200);
 ctx.closePath();
 ctx.stroke();
 const text = "Canvas";
 ctx.font = "24px monospace";
 const textWidth = ctx.measureText(text).width;
 const gradient = ctx.createLinearGradient(200,100,200 + textWidth,100);
 gradient.addColorStop(0,"magenta");
 gradient.addColorStop(1, "yellow");
 ctx.fillStyle = gradient;
 ctx.shadowColor = "transparent";
 ctx.fillText("Canvas", 200, 100);
 ctx.setLineDash([0]);
 ctx.strokeStyle = "gray";
 ctx.beginPath();
 ctx.moveTo(50,200);
 ctx.lineTo(50,250);
 ctx.lineTo(100,250);
 ctx.arcTo(100,200,50,200,50);
 ctx.stroke();
 ctx.beginPath();
 ctx.arc(275,150,50,1.57,3.14,false);
 ctx.lineTo(275,150);
 ctx.fill();
 ctx.globalAlpha = 0.75;
 ctx.beginPath();
 ctx.arc(175,75,40,0,6.28,false);
 ctx.clip();
 const image = new Image(100,100);
 image.onload = function() {
     ctx.drawImage(this, 125, 25, this.width, this.height);
 }
 image.src = "pluto.jpg";

以下图表显示了结果。您可以尝试运行完整的代码,该代码可在 Examples/example-8-canvas.html 中找到:

图片

在 HTML Canvas 上下文中绘制的某些形状。代码:Examples/example-8-canvas.html

以下表格列出了一些基本的 Canvas 命令。所有命令都是当前 Canvas 上下文的方法:

方法或属性 描述
fillStyle 设置在 fill() 命令中使用的颜色。
strokeStyle 设置在 stroke() 命令中使用的颜色。
lineWidth 设置在 stroke() 命令中使用的线宽。
lineCap 设置线条端点的样式,例如 butt(默认)、roundsquare
lineJoin 设置线条连接的样式,例如 ‘round'‘bevel'‘miter'(默认)。
font 设置在 strokeText()fillText() 命令中使用的字体。
globalAlpha 设置上下文的全局不透明度(0 = 透明,1 = 不透明)。
shadowBlur, shadowColor, shadowOffsetX, shadowOffsetY 设置阴影属性。默认颜色是透明的黑色。默认数值为零。
fillRect(x,y,w,h) 填充一个矩形。
strokeRect(x,y,w,h) 在矩形周围绘制一个边框。
setLineDash(dasharray) 接收一个用于虚线的数组,交替绘制线条和空白。
fillText(text,x,y); 在 x 和 y 位置填充文本(y 是基线)。
strokeText(text, x, y); 在 x 和 y 位置绘制文本的边框。
createLinearGradient(x0, y0, x1, y1) 创建与线条垂直的线性渐变。也支持径向渐变和图案。
drawImage(image, x, y, w, h) 绘制图像。
beginPath() 开始路径。
moveTo(x, y) 将光标移动到路径中的某个位置。
lineTo(x, y) 将光标移动到路径中的某个位置,同时绘制一条线。
bezierCurveTo(c1x, c1y, c2x, c2y, x, y), quadraticCurveTo(cx, cy, x, y) 在路径中绘制具有一个(二次)或两个(贝塞尔)控制点的曲线。
arc(x, y, r, sa, ea) 通过指定路径中的中心、半径、起始角和结束角来绘制弧线。
arcTo(sx, sy, r, ex, ey) 通过指定起始点坐标、半径和终点坐标来绘制弧线。
rect(x, y, w, h) 使用左上角的坐标、宽度和高度在路径中绘制矩形。
clip() 使用路径绘制的形状创建一个裁剪区域,该区域将影响之后绘制的对象。
fill() 使用当前颜色填充路径。
stroke() 使用当前颜色绘制路径。

选定的 HTML Canvas 命令

数据格式

用于可视化的数据通常以标准格式分布,可以共享。即使数据来自数据库,数据通常也以某种标准格式提供。流行的专有格式,如 Excel 电子表格,很常见,但大多数统计数据是以 CSV、XML 或 JSON 格式存储或提供的。

CSV

CSV 代表逗号分隔值。它是一种非常流行的公共数据格式。CSV 文件是一个文本文件,模拟表格。它通常包含一个标题行,包含列名,以及一个或多个包含值字段的数据行。行由换行符分隔,每行中的逗号分隔字段形成列。它与 HTML 表格完美对应。这是一个简单的 CSV 文件,包含七个大洲的人口和陆地面积(Data/sample.csv):

 continent,population,areakm2
 "North America",579024000,24490000
 "Asia",4436224000,43820000
 "Europe",738849000,10180000
 "Africa",1216130000,30370000
 "South America",422535000,17840000
 "Oceania",39901000,9008500
 "Antarctica",1106,13720000

在 CVS 中没有类型。引号用于包含可能包含分隔符的文本。如果字段不包含逗号,则不需要引号。

CSV 也用于指代不使用逗号作为分隔符的类似文件。这些文件更准确地称为分隔符分隔值DSV)文件。最常见的分隔符是制表符(TSV)、竖线(|)和分号。

CSVs 可能变得损坏且无法读取,但它是文本,你可以修复它。缺失或未转义的逗号是最常见的问题。

XML

可扩展标记语言XML)是一种非常流行的数据格式。Web 服务的 Ajax 响应通常以文本或 XML 返回。它通过 DOM API 在 JavaScript 中具有标准原生支持,不需要额外的解析。尽管仍然可以在 XML 格式中找到数据,但如果可用,CSV 和 JSON 替代品通常更小且更容易处理。

这是与前面显示的 CSV 文件相同数据的 XML 文件示例(Data/sample.xml):

<continents>
     <continent>
         <name>North America</name>
         <population>579024000</population>
         <area unit="km">24490000</area>
     </continent>
     <continent>
         <name>Asia</name>
         <population>4436224000</population>
         <area unit="km">43820000</area>
     </continent>
 ...
     <continent>
         <name>Antarctica</name>
         <population>1106</population>
         <area>13720000</area>
     </continent>
 </continents>

如果有 XML Schema 可用,XML 文件可以进行验证。你可以使用 DOM 或 XPath(这更容易)从格式良好的 XML 文件中提取数据。所有语言中都有许多工具可以用来操作 XML。XML 也很容易生成。它的主要缺点是冗长和体积大。

JSON

JSON 代表 JavaScript 对象表示法。它看起来很像 JavaScript 对象,但它有更严格的格式规则。这可能是最容易处理的一种格式。它紧凑且易于解析,并且正在逐渐取代 XML,成为 Web 服务中首选的数据格式。

包含大陆数据的以下数据文件以 JSON 格式(Data/sample.json)展示:

[
     {
         "continent": "North America",
         "population": 579024000,
         "areakm2": 24490000
     },{
         "continent": "Asia",
         "population": 4436224000,
         "areakm2": 43820000
     },{
         "continent": "Europe",
         "population": 738849000,
         "areakm2": 10180000
     },{
         "continent": "Africa",
         "population": 1216130000,
         "areakm2": 30370000
     },{
         "continent": "South America",
         "population": 422535000,
         "areakm2": 17840000
     },{
         "continent": "Oceania",
         "population": 39901000,
         "areakm2": 9008500
     },{
         "continent": "Antarctica",
         "population": 1106,
         "areakm2": 13720000
     }
 ]

JSON 是 JavaScript 中数据操作的首选格式。你可以使用许多在线工具将 CSV 和 XML 文件转换为 JSON。

加载和解析外部数据文件

除非你的数据集非常小或静态,否则通常不会将其嵌入到你的网页中。你可能会在 HTML 页面加载后,从单独的文件中异步请求加载它,然后解析它。本节涵盖了与加载和解析外部文件相关的话题。

使用 Web 服务器

本书中的大多数示例都由一个单独的文件组成(不考虑使用 <script> 标签加载的外部库),你只需在浏览器中打开它们即可运行。你甚至不需要 Web 服务器。只需点击文件,然后在浏览器中查看即可。但这种方法在通过 Ajax 加载外部文件的示例中不起作用。对于这些文件,你需要一个 Web 服务器。

如果你正在使用 HTML 编辑器,例如 PHPStorm 或 Brackets,它将自动为你启动一个 Web 服务器,并将页面服务到你的默认浏览器。如果你在系统中安装了 Python(它在 macOS 和 Linux 中是原生的,你可以在 Windows 中安装它),你可以在文件安装的目录中运行一个简单的服务器。语法取决于你安装的 Python 版本。你可以通过打开控制台并输入以下命令来检查:

python -v

现在,转到存储你的 HTML 文件的目录,并运行以下命令之一。如果你有 Python 3.x,运行:

python3 -m http.server

如果你使用的是 2.x 版本,运行:

python -m SimpleHTTPServer

现在,你可以使用 http://localhost:8080/your-file-name.html 打开你的文件。

使用标准 JavaScript 加载文件

将数据加载到网页中的标准方式是使用异步 JavaScript 和 XML,或 Ajax。它使用所有现代浏览器都支持的内置 XMLHttpRequest 对象。

要使用 XMLHttpRequest 加载文件,你需要创建 XMLHttpRequest 对象,选择一个 HTTP 方法,使用该对象打开到文件 URL 的 HTTP 连接,并发送请求。你还必须创建一个回调函数,该函数监听对象的 'readystatechange' 事件并测试对象的 readystate 属性。

当这个属性包含 XMLHttpRequest.DONE 时,请求已完成,你可以提取数据。但还没有结束!如果请求成功完成(对象 status 属性等于 200),你需要从对象中提取数据。在 CSV 文件中,数据将在 responseText 属性中(如果是 XML,则位于不同的位置)。只有在这种情况下,你才能最终解析其内容并创建你的数据数组。这在上面的代码(Examples/example-9-ajax.html)中展示:

 const httpRequest = new XMLHttpRequest();
 httpRequest.open('GET', 'Data/sample.csv');
 httpRequest.send();

    httpRequest.onreadystatechange = function(){
       if (httpRequest.readyState === XMLHttpRequest.DONE) {
           if (httpRequest.status === 200) {
               const text = httpRequest.responseText;
               parse(text);
           }
       }
    }
function parse(text) {
     // parse the CSV text and transform it into a JavaScript object
 }

使用 JQuery 加载文件

你永远不需要使用标准的 JavaScript 来加载文件,但了解它是如何工作的总是好的。使用 JQuery 库加载文件要简单得多得多(Examples/example-10-ajax-jquery.html):

$.ajax({
     url: 'Data/sample.csv',
     success: function(text){
         parse(text)
     }
 });

你也可以使用 JQuery 在单步中加载和解析 JSON 文件:

$.getJSON('/Data/sample.json', function(object) {
     // use the JavaScript object
 }

使用标准 Fetch API 加载文件

在所有现代浏览器中,你也可以使用 Fetch API 加载外部文件。这是加载文件的新的 JavaScript 标准,我们将在本书中所有加载外部文件的示例中使用它,但在一些较旧的浏览器中可能不起作用。在这种情况下,你应该回退到标准的 JavaScript 或 JQuery。

fetch() 命令是一个基于 JavaScript 承诺的响应式方法。一个基本的 fetch 请求示例如下(Examples/example-12-fetch.html):

fetch('Data/sample.csv')
  .then(response => response.text())
  .then(function(text) {
      parse(text);
  });

你也可以使用 fetch() 解析 JSON 文件:

fetch('Data/sample.json')
   .then(response => response.json())
   .then(function(object) {
       // use the JavaScript object
   });

解析 JSON

虽然 JSON 基于 JavaScript,但 JSON 文件不是一个 JavaScript 对象。它是一个字符串。为了将其转换为对象并使用点操作符访问其属性,你可以使用 JSON.parse()

const obj = JSON.parse(jsonString);

有时候你需要将 JavaScript 对象转换回 JSON 格式。你可能也会这样做以进行调试。你可以使用 JSON.stringify() 来实现:

const jsonString = JSON.stringify(obj);

如果你解析了本节开头示例中的 JSON 文件,JavaScript 对象实际上将是一个对象数组,你可以使用以下代码(Examples/example-14.html)列出其内容(在 JavaScript 控制台中):

obj.forEach(function(item) {
     console.log(item.continent, +item.population, +item.areakm2);
 });

解析 CSV

JavaScript 中没有内置的 CSV 解析器,但如果你有一个非常小且简单的 CSV 文件,你可以使用 JavaScript 字符串操作工具或正则表达式来解析它,通过换行符(\n)分割以选择每一行,然后通过分隔符分割以选择每一行中的数据单元格。

较大的数据文件更复杂,因为前面的代码依赖于特定的格式,并且没有处理引号字符串内的逗号、缺失数据等问题。在这种情况下,你应该使用 CSV 解析器。本书中的大多数示例使用由 Matt Holt 开发的 PapaParse CSV 解析器 (papaparse.com),它是开源的且免费的。以下代码展示了如何使用 PapaParse 将 CSV 转换为 JavaScript 对象:

const csvData = Papa.parse(csvText, {header: true}).data;

如果你解析了本节开头示例中的 CSV 文件,你将收到一个对象数组,你可以使用以下代码(Examples/example-15.html)列出其内容(在 JavaScript 控制台中):

csvData.forEach(function(item) {
     console.log(item.continent, +item.population, +item.areakm2);
 });

最后两个属性前的加号将它们转换为数字。如果你不这样做,即使它们是数字,它们也会被加载为字符串。

加载多个文件

有时你需要从不同的来源获取文件,这些文件需要在页面上加载并操作。你可以使用Promise.all()来加载这些文件,如下所示。Promise 中的代码只有在所有文件都加载完成后才会执行(Examples/example-16.html):

 const files = ['/path/to/file.json', '/path/to/file.csv'];
 var promises = files.map(file => fetch(file).then(resp => resp.text()));
 Promise.all(promises).then(results => {
     const jsonData = JSON.parse(results[0]);
     const csvData = Papa.parse(results[1], {header: true}).data;
     // use the two data objects
 });

显示地图

不使用任何图表库,仅使用标准 JavaScript,你可以加载一个 JSON 文件,并使用 Canvas 绘制世界地图。数据是特殊的 JSON 格式,用于存储地理形状:GeoJSON。其一般结构如下:

{"type":"FeatureCollection",
  "features":[
     {"type":"Feature",id":"AFG","properties":{"name":"Afghanistan"},
      "geometry":{"type":"Polygon","coordinates":[[[61.210817,35.650072],...]]
     },{"type":"Feature", "id":"AGO", "properties":{"name":"Angola"},
      "geometry":{"type":"MultiPolygon","coordinates":[[[[16.326528,-5.87747,...]]
     },
     // many other lines
   ]
 }

使用 JavaScript,你可以加载这个文件,解析它,并访问每个经纬度对。然后你可以调整这些值,使它们适合你的 Canvas 坐标系统,并使用 Canvas 路径命令绘制每个形状。这是在以下代码中完成的(Examples/example-17.html):

<canvas id="map" width="1000" height="500"></canvas>
<script>
     var canvas = document.getElementById("map");
     var ctx = canvas.getContext("2d");

     // Map ocean background
     ctx.fillStyle = 'white';
     ctx.fillRect(0, 0, canvas.width, canvas.height);

     // countries border and background
     ctx.lineWidth = .25;
     ctx.strokeStyle = 'white';
     ctx.fillStyle = 'rgb(50,100,150)';

     // load and draw map
     fetch('Data/world.geojson')
     .then(response => response.text())
     .then((jsonData) => {
         let object = JSON.parse(jsonData);
         drawMap(object.features);
     });
     function scaleX(coord) {
         return canvas.width * (180 + coord) / 360;
     }
     function scaleY(coord) {
         return canvas.height * (90 - coord) / 180;
     }
     function drawPolygon(coords) {
         ctx.beginPath();
         for(let i = 0; i < coords.length; i++ ) {
             let latitude = coords[i][1];
             let longitude = coords[i][0];
             if(i == 0) {
                 ctx.moveTo(scaleX(longitude), scaleY(latitude));
             } else {
                 ctx.lineTo(scaleX(longitude), scaleY(latitude));
             }
         }
         ctx.stroke();
         ctx.fill();
     }
     function drawMap(data) {
         data.forEach(obj => {
             if(obj.geometry.type == 'MultiPolygon') {
                 obj.geometry.coordinates.forEach(poly => drawPolygon(poly[0]));
             } else if(obj.geometry.type == 'Polygon') {
                 obj.geometry.coordinates.forEach(poly => drawPolygon(poly));
             }
         });
     }
 </script>

结果如下所示:

图片

使用 GeoJSON、JavaScript 和 Canvas 代码创建的世界地图

提取和转换数据

如果你足够幸运,能在 CSV、XML 或 JSON 中找到你的数据,你可以立即加载并开始使用。但如果你的数据仅以 HTML 表格的形式提供,或者更糟糕的是,以 PDF 文件的形式提供呢?在这些情况下,你需要提取你的数据并将其转换为可用的格式。

如果这是一个非常简单的 HTML 表格,有时你可以选择它并将其复制粘贴到电子表格中,以保留行和列。然后你可以将其导出为 CSV。有时你可能需要做额外的工作,比如删除垃圾字符、样式和不需要的列。这是有风险的,因为你可能在处理过程中丢失数据或引入错误。

在线工具

你还可以使用在线工具,尝试将 HTML 表格转换为 XML、CSV 和 JSON。让我们试一个例子。NASA JPL 网站有一个包含关于月球和太阳系内行星数据的网页(nssdc.gsfc.nasa.gov/planetary/factsheet)。要使用这些数据,你需要将其转换为标准格式,如 JSON、CSV 或 XML,但它只以 HTML 表格的形式提供,如下所示:

图片

包含可用于图表的数据的 HTML 表格

让我们先尝试一个在线转换服务。搜索 HTML 到 CSV 转换,我在www.convertcsv.com找到了一个在线转换服务,有几个 CSV 转换工具。打开 HTML 表格到 CSV 链接,然后可以在输入框中粘贴源代码,或者提供其 URL。有一些选项你可以配置,例如选择分隔符。点击“将 HTML 转换为 CSV”按钮,以下文本将出现在输出框中:

,MERCURY,VENUS,EARTH,MOON,MARS,JUPITER,SATURN,URANUS,NEPTUNE,PLUTO
 Mass (1024kg),0.330,4.87,5.97,0.073,0.642,1898,568,86.8,102,0.0146
 Diameter (km),4879,"12,104","12,756",3475,6792,"142,984","120,536",...,2370
 Density (kg/m3),5427,5243,5514,3340,3933,1326,687,1271,1638,2095
 Gravity (m/s2),3.7,8.9,9.8,1.6,3.7,23.1,9.0,8.7,11.0,0.7
 ... several rows not shown ...
 Number of Moons,0,0,1,0,2,79,62,27,14,5
 Ring System?,No,No,No,No,No,Yes,Yes,Yes,Yes,No
 Global Magnetic Field?,Yes,No,Yes,No,No,Yes,Yes,Yes,Yes,Unknown
 ,MERCURY,VENUS,EARTH,MOON,MARS,JUPITER,SATURN,URANUS,NEPTUNE,PLUTO

这是一个有效的 CSV 文件,但一些字段被解释为字符串,而不是数字(例如,一些直径)。你可能还希望删除一些不必要的行,例如最后一行,或者你不需要的数据。你可以在以后编辑文件并编写一个脚本,使用正则表达式修复数字。下载结果并将其保存到文件中,然后尝试使用 JavaScript 加载该文件。

由于这是一个第三方在线服务,我无法保证你在阅读这本书时它仍然存在,但你应该能找到执行相同转换的类似服务。如果找不到,你也可以自己编写一个提取脚本。用于此目的的一个好工具是 XPath,它被许多提取库和浏览器支持,将在下一节中描述。

使用 XPath 提取数据

由于 HTML 是一个结构化文档,你可以使用计算机程序来导航这个结构并提取选定的文本节点、属性和元素。大多数网络提取工具都是基于 XPath 的:这是一个 XML 标准,可以用来在 XML 结构中导航并使用路径符号选择元素、属性和文本节点。尽管 HTML 不像 XML 那样严格,但它有类似的可以表示为 XPath 路径的结构,并且被许多网络抓取工具支持。

例如,上一页面的前几行具有以下结构:

<html>
   <head>
     <title>Planetary Fact Sheet</title>
   </head>
   <body bgcolor=FFFFFF>
     <p>
     <hr>
     <H1>Planetary Fact Sheet - Metric</H1>
     <hr>
     <p>
     <table> ...

这不是 XML 或 XHTML,因为属性不在引号内,标签也没有关闭,但你仍然可以使用 XPath 从它中提取数据。此路径将给出标题:

/html/head/title/text()

以下任何一个都可以从 body 标签返回 bgcolor 属性(其名称和值):

/html/body/@bgcolor
/html/body/attribute::bgcolor

这个将返回 <H1> 标题的内容:

/html/head/h1/text()

这个有点棘手。如果这是 XML,它将是 /html/head/p/hr/H1,因为所有 XML 标签都必须关闭,但 HTML 解析器会自动关闭 <p><hr> 标签,因为其中不能有 <h1> 标题。HTML 也不区分大小写,所以使用 H1h1 对这些解析器来说没有区别。尽管如此,这仍然可能会让一些解析器感到困惑。你可以通过使用以下方法来确保安全:

/html/head//H1/text()

// 或双斜杠表示在 <head><H1> 之间可以有任意数量的层级。这与 XML 或 HTML 绝对路径兼容。

你可以使用浏览器中的 JavaScript 控制台来实验 XPath,在 $x(expression) 内写入 XPath 表达式。让我们用 行星事实表 页面来试一试。在浏览器中打开页面,然后打开一个控制台窗口,并输入以下内容:

$x("//table")

这将选择文档中的所有表格。在这种情况下,只有一个。你也可以查看源代码或检查页面以发现绝对路径:

$x("/html/body/p/table")

输入此命令,控制台将显示与你的选择对应的 HTML 片段。现在让我们选择包含直径的行。它是表格中的第三行。你可以忽略现有的<thead><tbody>标签,使用//。XPath 从 1 开始计数子节点,而不是 JavaScript 中的 0。命令返回一个包含单个<tr>元素的数组。我们可以使用[0]来提取它:

$x("//table//tr[3]")[0]

这将选择以下片段:

<tr>
   <td align="left"><b><a href="planetfact_notes.html#diam">Diameter</a> 
(km)</b></td>
   <td align="center" bgcolor="F5F5F5">4879</td>
   <td align="center" bgcolor="FFFFFF">12,104</td>
   <td align="center" bgcolor="F5F5F5">12,756</td>
   <td align="center" bgcolor="FFFFFF">3475</td>
   <td align="center" bgcolor="F5F5F5">6792</td>
   <td align="center" bgcolor="FFFFFF">142,984</td>
   <td align="center" bgcolor="F5F5F5">120,536</td>
   <td align="center" bgcolor="FFFFFF">51,118</td>
   <td align="center" bgcolor="F5F5F5">49,528</td>
   <td align="center" bgcolor="FFFFFF">2370</td>
 </tr>

要选择地球的直径,你需要添加一个额外的路径步骤:

$x("//table//tr[3]/td[4]")[0]

结果如下:

<td align="center" bgcolor="F5F5F5">12,756</td>

要提取文本,你需要在 XPath 表达式的末尾包含text()函数。你还需要从$x()函数的结果中提取数据,使用data属性:

const result = $x("/html/body/p/table/tbody/tr[3]/td[4]/text()")[0].data

这将返回一个字符串结果。然后你可以使用正则表达式删除逗号,然后将结果转换为数字:

const value = +data.replace(/\,/g,''); 
// removes commas and converts to number

如果你需要提取大量数据,例如所有行星的直径,你可能想使用编程库来自动化这个过程。$x()命令仅在浏览器控制台中有效,但许多编程语言支持 XPath 库和 API。你也可以使用像 Scrapy(Python 中)或 Selenium(多种语言中)这样的工具,这些工具支持 XPath 选择器,可以从 HTML 中提取数据。

XPath 是一种非常强大的数据提取语言,这只是一个非常简短的介绍。但也有一些替代方案,例如 XQuery(另一种具有查询语法的 XML 标准)和 CSS 选择器(由JQuery使用,也由 Scrapy 和 Selenium 支持)。

摘要

本章回顾了几个基本的技术概念,这些概念将帮助你使用 Chart.js 创建可视化。尽管 Chart.js 试图隐藏所有底层复杂性,但它仍然是一个 JavaScript 库,对 JavaScript、DOM 和 CSS 的基本知识仍然很重要。

本章还描述了用于统计数据的常用数据格式:CSV、XML 和 JSON。它还描述了如何加载这些格式的外部文件以及如何解析它们。此外,你还了解了一些从 HTML 页面中提取非这些格式数据的方法。

在下一章中,我们将开始使用 Chart.js 创建数据可视化。

第三章:Chart.js - 快速入门

本章提供了使用 Chart.js 创建基于 Web 的数据可视化的快速入门指南。您将学习如何设置库并配置一个基本网页,您可以在其中包含图表。我们将通过一个完整的分步示例,描述如何创建柱状图并使用标签、工具提示、标题、交互、颜色、动画等对其进行配置。通过使用 Chart.js 创建一个功能齐全的图表,您将更好地理解我们在稍后探索细节时的主要概念。

在本章中,您将了解以下主题:

  • 如何安装和设置 Chart.js

  • 如何创建一个简单的柱状图

  • 如何配置坐标轴、颜色和工具提示

  • 如何添加动画和响应简单事件

  • 如何创建水平和堆叠的柱状图

Chart.js 简介

Chart.js 是一个基于 HTML5 Canvas 的开源社区维护的 JavaScript 数据可视化库。在撰写本文时,它可用作版本 2.7.3,并包含八种可定制的图表类型。使用 Chart.js 创建图表非常简单。它可以简单到将 JavaScript 库加载到您的页面中,选择一个图表类型,并为其提供一个数据数组。

所有图表都使用默认的外观和感觉以及基本交互功能进行配置。您可以专注于数据,并快速创建一个简单、响应式且适合您页面的交互式图表。您不必担心配置填充或边距、将标签放入坐标轴、添加工具提示或控制过渡。但如果您需要更改某些内容,有许多配置选项可供选择。

安装和设置

要设置一个用于 Chart.js 的网页,您只需加载库。如果您已经设置了一个网站,您可以从www.chartjs.org下载 Chart.js,将其存储在您可以从网页加载的位置,并使用以下<script>标签导入它:

<script src="img/Chart.min.js"></script>

如果您有一个模块化开发环境,您也可以使用npmbower安装 Chart.js,如下面的代码所示。Chart.js 与 ES6 模块、AngularReactVue应用程序集成良好:

npm install chart.js --save
bower install chart.js --save

开始使用最简单的方式是链接到一个由内容分发网络CDN)提供的库文件。您可以在cdnjs.com/libraries/Chart.js获取列表,选择您想要的版本和 CDN 提供商,并复制其中一个链接。除非您想检查代码,否则请使用压缩链接(以min.js结尾)。最好不要使用捆绑版本,因为它包含一些额外的非 Chart.js 库(更好的做法是在需要时单独包含第三方库)。

使用任何文本或代码编辑器,将链接复制到 HTML 文件<head>部分中某个script标签的src属性,如下所示:

<script src="img/Chart.min.js">
</script>

如果你有一个开发环境,例如 Visual Studio Code、PHPStorm 或 Brackets,你可能希望为新页面设置一个包含 Chart.js CDN 脚本标签的模板文件。

另一种探索 Chart.js 和跟随本书中的示例的方法是使用在线代码编辑器,例如 CodePenJSFiddle。这也是分享你的图表和代码的好方法。

使用 JSFiddle (jsfiddle.net/),你只需要将 Chart.js CDN 添加到资源选项卡,如下面的截图所示,然后你可以使用选项卡来编写你的图表的 HTML、CSS 和 JavaScript 代码:

使用 JSFiddle (jsfiddle.net) 作为在线代码编辑器

要在 CodePen (codepen.io/) 中配置 Chart.js,请点击设置菜单,然后点击 JavaScript 选项卡。搜索 Chart.js,并点击第一个选项将 CDN 添加到你的环境中,如下面的截图所示:

将 Chart.js 支持添加到 CodePen (codepen.io)

你现在可以使用 Chart.js 库并实时查看结果,如下面的截图所示:

使用 CodePen (codepen.io) 作为在线代码编辑器

创建一个简单的柱状图

现在你已经设置好了工作环境,让我们开始创建一个简单的柱状图。你可以边走边输入代码,也可以从 GitHub 仓库下载本章的完整工作示例。本书中的每个截图和代码列表都包含用于生成它的文件引用。

设置图形上下文

图表是在由 HTML Canvas 对象提供的图形上下文中显示的。创建一个图表有很多方法;最简单的方法是使用纯 HTML。在 <body> 内部的某个位置放置一个 <canvas> 元素。它应该有一个 ID 属性,如下所示:

<canvas id="my-bar-chart" width="200" height="200"></canvas>

Chart.js 图形默认是响应式的。图表将适应可用空间:heightwidth 属性不会影响图表的实际大小(除非你更改默认值)。

你可以使用 DOM(或 JQuery)在任何由你的 HTML 文件加载的脚本块或文件中获取 canvas 对象的 JavaScript 处理器,如下面的代码片段所示(在本书的大多数 JavaScript 列表中,脚本块将被忽略):

<script>
    const canvas = document.getElementById("my-bar-chart");
</script>

你还可以使用 文档对象模型 (DOM) 或 JQuery 动态创建一个 <canvas> 对象。在这种情况下,ID 属性不是严格必要的,因为变量本身可以用作处理器的引用,但定义一个属性是良好的实践,如下所示:

canvas canvas = document.createElement("canvas");
canvas.setAttribute("id","my-bar-chart");
document.body.appendChild(canvas);

使用 Chart() 构造函数创建图表。它接收两个参数:图表将要显示的画布的图形 上下文,以及包含图表数据的 对象,如下面的代码所示:

const chartObj = {…}; // the chart data is here
const context = canvas.getContext("2d");
new Chart(context, chartObj); // this will display the chart in the canvas

如果你的 canvas 对象已经声明了 ID 属性,你不需要 context 对象。你可以直接使用 ID 属性作为第一个参数,如下所示:

new Chart("my-bar-chart", chartObj);

包含图表数据的对象至少需要两个属性:type,它选择 Chart.js 的八种不同类型的图表之一;以及 data,它引用一个包含数据集和要显示的数据属性的对象的引用,如下所示:

const chartObj = {type: "bar", data: dataObj};

通常,图表对象是在构造函数内部配置的,如下所示:

new Chart("my-bar-chart", {type: "bar", data: dataObj});

这是使用 Chart.js 创建的任何图表的基本设置。它目前不会显示任何图表,因为我们没有提供任何数据,但如果你的库正确加载,你应该看到一个空轴。代码位于 Templates/BasicTemplate.html 文件中。

如果你的屏幕上没有显示任何内容,那么你的代码中可能存在语法错误。检查浏览器中的 JavaScript 控制台。当你使用 JavaScript 时,始终打开它是好主意,这样错误可以快速检测和修复。

创建柱状图

柱状图显示与值相关联的分类列表,这些值由柱子的长度表示。要创建一个简单的柱状图,我们需要一个 分类 列表以及一个 列表。

让我们创建一个简单的图表来显示每个海洋的水量。我们需要一个分类数组,如下所示:

const labels = ["Arctic", "North Atlantic", "South Atlantic",
                "Indian", "North Pacific", "South Pacific",
                "Southern"];

此外,我们还需要一个相应的值数组,如下所示:

const volumes = [18750,146000,160000,264000,341000,329000,71800]; // 10³ km3

data 对象应该包含一个 labels 属性,它将引用 categories 数组,以及一个 datasets 属性,它包含一个至少有一个 dataset 对象的数组。每个 dataset 对象都有一个 label 属性和一个 data 属性,它将接收我们图表(volumes 数组)的数据,如下所示:

const dataObj = {
     labels: labels,
     datasets: [
         {
             label: "Volume",
             data: volumes
         }
     ]
}

图表已经预配置了刻度、轴、默认颜色、字体、动画和工具提示。将前述代码中的 dataObj 对象作为图表对象(chartObj)的 data 属性,你将得到一个交互式且响应式的柱状图,类似于以下所示:

一个简单的柱状图(代码:Pages/BarChart1.html)

尝试调整窗口大小,看看标签和刻度如何适应新的显示。将鼠标移到柱子上,看看工具提示如何显示图表信息。单击 dataset 标签并切换其可见性。在以下章节中,我们将配置一些其外观和行为。

完整的列表如下所示:

 <!DOCTYPE html>
 <html lang="en">
 <head>
 <script src="img/Chart.min.js">
 </script>
 </head>
 <body>
 <canvas id="ocean-volume-bar-chart" width="400" height="400"></canvas>
 <script>
   const labels = ["Arctic", "North Atlantic", "South Atlantic",
                   "Indian", "North Pacific", "South Pacific", "Southern"];
   const volumes = [18750,146000,160000,264000,341000,329000,71800];

   const dataObj = {
       labels: labels,
       datasets: [
           {
               label: "Volume",
               data: volumes
           }
       ]
   }
   new Chart("ocean-volume-bar-chart", {type: "bar", data: dataObj});
  </script>
 </body>
 </html>

尝试在你的开发环境中输入前述代码,或者从 Pages/BarChart1.html 下载它。

配置颜色、字体和响应性

使用 Chart.js 创建的图表天生具有响应性,并且很好地适应屏幕,但它们也是天生的灰色。在本节中,你将了解如何更改一些样式属性。

柱状图的 dataset 配置

除了datalabel属性外,每个数据集对象还可以包含许多可选配置属性。它们大多数用于配置填充和边框颜色和宽度。以下表格中简要描述了它们:

属性 描述
data Number[] 包含要显示的数据的数字数组(这是必需的)
label String 数据集的标签
backgroundColor StringString[] 条形的填充颜色
borderColor StringString[] 边框的颜色
borderWidth NumberNumber[] 边框的宽度
hoverBackgroundColor StringString[] 鼠标悬停在条形上时的填充颜色
hoverBorderColor StringString[] 鼠标悬停在条形上时的边框颜色
hoverBorderWidth NumberNumber[] 鼠标悬停在条形上时的边框宽度
borderSkipped bottom, left, top, right 选择条形没有边框的边缘(默认为barbottomhorizontalBarleft
yAxisIDxAxisID 轴 ID(见第五章,刻度和网格配置 用于轴配置

条形图的数据集属性

您可以通过在每个dataset对象中添加颜色属性或通过配置影响所有图表的全局默认值来更改灰色条形。backgroundColor属性接收一个包含以合法 CSS 格式指定的颜色的字符串。例如,如果您想有实心的红色条形,可以使用以下格式:

  • red

  • rgb(255,0,0)

  • rgba(100%,0,0,1)

  • #ff0000, #f00

  • hsl(0,100%,50%)

  • hsla(0,100%,50%,1)

borderColor属性控制标签图标的颜色。如果指定了大于零的borderWidth值,它还会配置条形的颜色,如下所示:

 const dataObj = {
     labels: labels,
     datasets: [
         {
             label: "Volume",
             data: volumes,
             borderWidth: 2,
             backgroundColor: "hsla(20,100%,80%,0.8)",
             borderColor: "hsla(0,100%,50%,1)"
         }
     ]
 }
 new Chart("ocean-volume-bar-chart", {type: "bar", data: dataObj});

上述代码应产生以下所示的结果。完整代码可在Pages/BarChart2.html中找到:

将颜色属性应用于条形图(代码:Pages/BarChart2.html)

选项配置

默认值是按图表配置的,包含在chart对象中的选项配置对象(构造函数的第二个参数),如下面的代码块所示:

new Chart("ocean-volume-bar-chart",
        {
            type: "bar",
            data: dataObj,
            options: {} // configure options here
        });

您可以更改许多默认值。例如,您可能希望对图表的大小有更多的控制,图表会自动调整大小。这是因为图表默认是响应式的。您可以通过覆盖responsive属性来关闭响应式,该属性的默认值为true,如下所示:

options: {
    responsive: false
}

现在,您的图表不再自动调整大小。但是,如果您想进行调整大小,但不关心纵横比怎么办?那么,您可以覆盖maintainAspectRatio属性,如下所示:

options: {
    maintainAspectRatio: false
}

如果您的canvas对象位于一个控制其大小并使用 CSS 配置的父<div>容器内,您可能希望这样做。在以下代码(Pages/BarChart4.html)中,画布将占据其父容器大小的80%,如下所示:

<style>
    #canvas-container {
        position: relative;
        height: 80%;
        width: 80%;
    }
</style>
<div id="canvas-container">
  <canvas id="ocean-volume-bar-chart" width="200" height="200"></canvas>
</div>

如果您尝试调整图表大小,默认情况下,它将保持其宽高比(并且不再适应页面),除非将maintainAspectRatio属性设置为false,如下所示:

配置图表的宽高比以使其适应画布(代码:Pages/BarChart4.html

文本和字体

文本可以包含在许多不同的对象中。每个数据对象可以有一个类别列表,每个数据集可以有一个图例,图表可以有一个标题,并且工具提示可以包含标题和其他信息。某些文本默认是可见或不可见的。您可以为图表内的任何文本配置可见性、字体家族、字体大小和颜色。

字体可以全局应用(对所有图表)使用默认配置。使用options配置对象,它们可以局部应用于标题和图例,这些也被配置为对象。

在我们的图表中,我们有一个数据集,这使得图例变得多余。您可以通过将legend.display属性更改为false来隐藏它,如下所示:

options: { // configure options here
    ...
    legend: {
        display: false
    }
}

我们还可以为图表添加标题并配置其字体大小、颜色和家族,如下所示:

options: { // configure options here
     maintainAspectRatio: false,
     legend: {
         display: false
     },
     title: {
         display: true,
         text: ['Volume of the oceans','in thousands of cubic km'],
         fontFamily: "TrebuchetMS",
         fontSize: 24,
         fontColor: 'rgb(0,120,0)',
     }
 }

仅将标题添加到text属性是不够的。由于display属性默认为false,您必须显式将其定义为true,以便标题显示。这些更改后,您的图表应类似于以下所示:

添加标题并定义字体属性(代码:Pages/BarChart5.html

您可以配置图表中所有文本的样式,包括单个工具提示标题和正文、刻度、主刻度和副刻度,正如我们将在以下章节中看到的。

全局默认值

本地配置选项会覆盖全局默认配置,这些配置可以使用Chart.defaults.global对象进行配置。您可以为字体、颜色、坐标轴、网格线、刻度、动画、工具提示和元素属性进行配置,无论是全局(对所有图表)还是本地(对特定图表)。

要指定全局字体家族,您可以使用以下代码:

Chart.defaults.global.defaultFontFamily = "Helvetica Neue";

这将影响图表中的所有文本。您还可以通过更改属性(如Chart.defaults.global.legendChart.defaults.global.title等)为特定的文本元素定义默认值,如下所示:

Chart.defaults.global.legend.fontSize = 10; // all legend text will be size 10

图表中使用的默认颜色是rgba(0,0,0,0.1)(浅灰色)。您可以使用Chart.defaults.global.defaultColor来更改此颜色。

全局属性非常适合包含在单独的.js文件中,这样您的图表就具有一致的外观和字段。您也可能更喜欢在只有一个图表的情况下也使用它们。

以下表格列出了可以应用于局部或全局的条形图特定配置。全局条形图设置存储在Chart.defaults.barCharts.defaults.horizontalBar中。局部设置应存储在options对象下的scales.xAxes[]scales.yAxes[]中,分别对应垂直和水平条形图。尝试在您的图表中使用其中一些,并查看您得到的结果:

属性 描述
barPercentage Number 条形(每个数据集)宽度占类别(所有数据集)宽度的百分比。默认值为0.9
categoryPercentage 类别宽度占样本宽度的百分比。默认值为0.8
barThickness 这将手动设置条形宽度(忽略categoryPercentagebarPercentage)。
maxBarThickness 这将条形厚度限制为此数值。

图表的选项配置属性

过渡、交互和工具提示

所有图表也都自带基本过渡、动画和交互式工具提示。对于简单的图表,您可能不需要更改任何内容;但如果您想要更多控制,可以使用局部和全局属性来配置这些行为。

过渡持续时间

您可以创建在用户交互时改变外观的图表。它们将自动优雅且平滑地过渡到新值。过渡动画使用默认的简单算法和持续时间配置,但您可以通过编辑Chart.defaults.global.animation对象的属性来更改它们,或者通过使用options.animation对象在本地覆盖任何默认设置。

例如,在以下图表代码中,所有过渡持续五秒钟(Pages/BarChart6.html):

new Chart("ocean-volume-bar-chart", {
    type: "bar",
    data: {...},
    options: {
        …
        animation: {
            duration: 5000
        }
    }
});

更新图表

您可以使用 JavaScript 函数和库动态更改数据,但这些更改不会立即反映在您的图表中。更改数据后,您必须调用update()以重新绘制它。为此,您需要一个对chart对象的变量引用,如下所示:

const ch = new Chart("ocean-volume-bar-chart", {...});

以下示例切换图表中的数据,用不同的数组替换数据集中的值,并更改标签、标题和颜色。toggle()函数被注册为画布上的点击事件监听器。无论何时在画布上点击,它都会运行,更改几个属性的值,并调用update(),这将强制图表过渡到新的数据和外观,如下所示:

const labels = ["Arctic", "North Atlantic", "South Atlantic", "Indian",
                "North Pacific", "South Pacific", "Southern"];

 const area   = [15558,41900,40270,70560,84000,84750,21960];   // km2 * 10³
 const volume = [18750,146000,160000,264000,341000,329000,71800];      //km3 * 10³
 const canvas = document.getElementById("ocean-volume-bar-chart");
 const ctx = canvas.getContext("2d");
 const ch = new Chart(ctx, {
     type: "bar",
     data: {
         labels: labels,
         datasets: [
             {
                 label: "Volume",
                 data: volume,
                 borderWidth: 2,
                 backgroundColor: "hsla(20,100%,80%,0.8)",
                 borderColor: "hsla(0,100%,50%,1)"
             }
         ]
     },
     options: {
         maintainAspectRatio: false,
         title: {
             display: true,
             text: ['Volume of the oceans','in thousands of cubic km'],
             fontFamily: "TrebuchetMS",
             fontSize: 24
         },
         legend: {
             display: false
         }
     }
 });

 canvas.addEventListener("click", toggle);

 function toggle(event) {
     if(ch.data.datasets[0].label == "Volume") {
         ch.data.datasets[0].data = area;
         ch.data.datasets[0].label = "Area";
         ch.data.datasets[0].borderColor = "hsla(120,100%,50%,1)";
         ch.data.datasets[0].backgroundColor = "hsla(140,100%,80%,0.8)";
         ch.options.title.text = ['Surface area of the oceans',
                                  'in thousands of square km'];
     } else {
         ch.data.datasets[0].data = volume;
         ch.data.datasets[0].label = "Volume";
         ch.data.datasets[0].backgroundColor = "hsla(20,100%,80%,0.8)";
         ch.data.datasets[0].borderColor = "hsla(0,100%,50%,1)";
         ch.options.title.text = ['Volume of the oceans',
                                  'in thousands of cubic km'];
     }
     ch.update();
 }

以下截图显示了点击前后相同的图表。完整代码可在Pages/BarChart7.html找到:

点击前后相同图表的截图。代码:Pages/BarChart7.html

工具提示

动画持续时间不影响工具提示,它们有自己的配置。除了动画之外,你还可以在工具提示中配置颜色、字体、间距、形状和行为。你还可以声明回调函数,在每个交互时改变外观和内容。如果你需要向工具提示添加更多信息,Chart.js 允许你创建包含图像和文本的复杂 HTML 工具提示。

例如,以下配置创建包含默认标题颜色的黑色工具提示。工具提示配置选项包含设置文本颜色的回调,使其与条形图的颜色相匹配,如下所示:

options: {
     …
     title: {…},
     legend: {…},
     animation: {…},
     tooltips: {
       backgroundColor: 'rgba(200,200,255,.9)',
       titleFontColor: 'black',
       caretSize: 5,
       callbacks: {
           labelColor: function(tooltipItem, chart) {
              return {
                  borderColor: 'black',
                  backgroundColor: 
                  chart.data.datasets[0].backgroundColor
              }
           },
           labelTextColor:function(tooltipItem, chart){
              return chart.data.datasets[0].borderColor;
           }
        }
     }
 }

你可以在Pages/BarChart8.html文件中运行前面的代码。

与更大的多个数据集一起工作

到目前为止我们所看到的,你应该已经能够创建一个简单的条形图。在本节中,我们将探讨一些与大型数据集相关的配置选项,你可能会将其作为外部文件加载,以及多个数据集,这些数据集可以绘制在同一张图表上。

加载数据

许多时候,你的数据将在线上可用,你可能希望动态地加载它。将数据和代码保存在不同的文件中也是一个好主意。如果你有一个 CSV 文件中的数据,你可以将其加载到 JavaScript 代码中,并使用它来生成图表。

JavaScript 使用 Ajax 异步加载数据。你可以使用标准的 Ajax、JQuery 或 ES6 fetch 函数,它像一个 JavaScript 承诺一样工作。在加载 CSV 文件后,你需要解析它。如果你只需要一组类别标签和值,你可以不使用解析器来处理。

在这个例子中,我们将使用一个包含 20 个最大污染者在海洋中处置的塑料废物量的 CSV 文件。你可以在本章的 GitHub 仓库中找到以下代码Data/waste.csv

 Country,Tonnes
 China,8819717
 Indonesia,3216856
 Philippines,1883659
 ...
 United States,275424

以下代码加载并解析文件,将数据拆分为行,然后通过逗号拆分每一行来组装一个labels数组和values数组(我们也可以使用 CSV 解析器)。这个过程将数据转换成可以由 Chart.js 使用的数组格式,如下所示:

fetch('../Data/waste.csv')
  .then(response => response.text())
  .then((data) => {
    const labels = [],
          values = [];
    const rows = data.split("\n");

    rows.forEach(r => {
        const item = r.split(",");
        labels.push(item[0]);
        values.push(+item[1]);
    });

    labels.shift(); // remove the header
    values.shift(); // remove the header

    console.log(labels); // print to check if the arrays
    console.log(values); // were generated correctly

    draw(labels, values);

 });

draw()函数包含设置画布、创建和显示条形图的代码,如下所示:

function draw(labels, values) {
   const canvas = document.getElementById("bar-chart");
   const ctx = canvas.getContext("2d");

   new Chart(ctx, {
       type: "bar",
       data: {
           labels: labels, // the labels
           datasets: [
               {
                   label: "Tonnes of plastic",
                   data: values, // the data values
                   borderWidth: 2,
                   backgroundColor: "hsla(20,100%,80%,0.8)",
                   borderColor: "hsla(0,100%,50%,1)"
               }
             ]
         },
         options: {
             maintainAspectRatio: false,
             title: {
                 display: true,
                 text: 'Tonnes of plastic waste',
                 fontSize: 16
             },
             legend: {
                 display: false
             }
         }
     });
 }

你可以在Pages/BarChart9.html中查看完整的代码。结果如下所示:

图片

使用从外部文件加载数据创建的条形图(代码:Pages/BarChart9.html)

水平条形图

当你需要显示和比较大量数据时,水平条形图可能更适合。你可以通过将type更改为horizontalBar轻松地将垂直条形图转换为水平条形图,如下所示:

new Chart(ctx, {
    type: "horizontalBar",
    data: {…}
}

前面的图表似乎更适合作为水平图表,因为类别标签不需要侧转。你可以在Pages/BarChart10.html中看到完整的代码。以下截图显示了图表现在的样子:

图片

水平条形图(代码:Pages/BarChart10.html

添加额外数据集

你可以向条形图中添加更多数据集,并使用新的图例标签、颜色和数据数组进行配置。以下示例将加载一个较大的 .csv 文件,其中包含 2010 年塑料废物处理的数据和 2025 年的预测。它有一个额外的列,如下所示:

 Country,2010,2025
 China,8819717,17814777
 Indonesia,3216856,7415202
 Philippines,1883659,5088394
 ...
 United States,275424,336819

这次,代码将生成两个 data 数组和单个 labels 数组。属于同一类别的数据和标签具有相同的索引,如下所示:

fetch('../Data/waste2.csv')
 .then(response => response.text())
 .then((data) => {
   const labels = [],
         values2010 = [],
         values2025 = [];

   const rows = data.split("\n");

   rows.forEach(r => {
       const item = r.split(",");
       labels.push(item[0]);
       values2010.push(+item[1]/1000000); // divide by 1 million to make
       values2025.push(+item[2]/1000000); // the chart easier to read
    });

    labels.shift();
    values2010.shift();
    values2025.shift();

    draw(labels, [values2010, values2025]);

 });

新的值将包含在第二个数据集中,在 datasets 数组中,如下所示。

function draw(labels, values) {
    const canvas = document.getElementById("bar-chart");
    const ctx = canvas.getContext("2d");

    new Chart(ctx, {
        type: "horizontalBar",
        data: {
            labels: labels,
            datasets: [
                {  
                    label: "2010",
                    data: values[0],
                    backgroundColor: "hsla(20,100%,50%,0.7)",
                },{
                    label: "2025",
                    data: values[1],
                    backgroundColor: "hsla(260,100%,50%,0.7)",
                }
            ]
        },
        options: {
            maintainAspectRatio: false,
            title: {
                display: true,
                text: 'Millions of tonnes of plastic waste',
                fontSize: 16
            }
        }
    });
 }

完整的代码位于 Pages/BarChart12.html。使用两个数据集,每个类别都有两个条形。每个数据集还有一个图例项。结果如下所示:

图片

包含两个数据集的条形图(代码:Pages/BarChart12.html

使用两个或更多数据集时,你可能希望使用配置选项属性 barPercentagecategoryPercentage 来配置条形的宽度。前者控制每个类别的单个条形的宽度,后者确定一个类别中所有条形所占的空间。这些属性应该在 options.scales.xAxes[] 中定义,如果你使用的是条形图,如果是 horizontalBar(见 Pages/BarChart12.html),则应在 options.scales.yAxes[] 中定义,如下所示:

options: {
    maintainAspectRatio: false,
    title: {
        display: true,
        text: 'Tonnes of plastic waste',
        fontSize: 16
     },
     scales: {
         yAxes: [{
             barPercentage: .3,
             categoryPercentage: .5
         }]
     }
 }

堆叠条形图

条形通常并排放置以进行比较。然而,如果值是整体的一部分,你可以将不同数据集中的条形堆叠起来以强调这种关系。我们可以堆叠世界海洋的体积,因为它们的总和揭示了世界上海洋水的总体积。以下数据对象将每个海洋的体积放置在单独的数据集中,如下所示:

const dataObj = {
    labels: ["Volume"], // there is only one category
    datasets: [
        {
            label: "Arctic", data: [18750],
            backgroundColor: "hsla(0,100%,50%,0.5)"
        },{
            label: "North Atlantic", data: [146000],
            backgroundColor: "hsla(60,100%,50%,0.5)"
        },{
            label: "South Atlantic", data: [160000],
            backgroundColor: "hsla(120,100%,50%,0.5)"
        },{
            label: "Indian", data: [264000],
            backgroundColor: "hsla(180,100%,50%,0.5)"
        },{
            label: "North Pacific", data: [341000],
            backgroundColor: "hsla(240,100%,50%,0.5)"
        },{
            label: "South Pacific", data: [329000],
            backgroundColor: "hsla(300,100%,50%,0.5)"
        },{
            label: "Southern", data: [71800],
            backgroundColor: "hsla(340,100%,50%,0.5)"
        },
    ]
 };

要将条形图转换为堆叠图,你必须配置 xy 轴的设置,并启用堆叠属性,如下所示:

 const optionsObj = {
    maintainAspectRatio: false,
     title: {
         display: true,
         text: 'Volume of oceans (km3)',
         fontSize: 16
     },
     legend: {
         position: 'right'
     },
     scales: {
         xAxes: [{
             stacked: true
         }],
         yAxes: [{
             stacked: true
         }]
     }
 }
 new Chart("ocean-volume-bar-chart",
           {type: "bar", data: dataObj, options: optionsObj});

预期的结果如下所示。完整的代码位于 Pages/BarChart13.html

图片

可以使用堆叠条形图来显示数据作为整体的一部分(代码:Pages/BarChart13.html

摘要

在本章中,我们学习了如何在 Web 应用程序中安装 Chart.js 以及如何快速创建一个简单、交互式且响应式的条形图,并将其包含在网页中。我们还学习了如何通过更改基本属性,如颜色、字体、响应性、动画持续时间和工具提示来配置图表的默认外观和感觉。有了这些知识,你就可以开始使用 Chart.js 在你的网页中显示简单的数据可视化。

本章还探讨了某些现实世界的问题,例如加载外部文件,以及如何通过过滤数据和配置图表来处理更大的数据集,以便更有效地显示信息。

在接下来的章节中,我们将更详细地探讨 Chart.js,创建所有八种不同的图表类型,学习如何配置其他几个属性,并处理更复杂的数据集。

参考文献

书籍和网站:

数据来源:

  • 世界海洋的体积(基于 ETOPO1):Chapter03/Pages/BarChart1.html以及其他。www.ngdc.noaa.gov/mgg/global/etopo1_ocean_volumes.html

  • 塑料垃圾:Chapter03/Data/waste.csv。Jambeck 等人发表的《塑料垃圾从陆地进入海洋的输入》。科学杂志,2015 年 2 月 13 日

第四章:创建图表

本章介绍了使用 Chart.js 创建的几个图表,这些图表可以有效地传达定量信息和关系。图表的选择取决于数据的类型,每一组值之间的关系,以及你想要展示的关系类型。在前一章中,我们学习了如何高效地在柱状图中展示数据,并比较不同类别相关的定量信息。在本章中,你将创建折线图和雷达图来比较一维数据的序列,饼图和环形图来比较比例,散点图和气泡图来表示两个或更多维度,以及极坐标图来在径向网格中显示定量数据。

在本章中,你将了解以下主题:

  • 折线和面积图

  • 雷达图和极坐标图

  • 饼图和环形图

  • 散点图和气泡图

折线和面积图

折线图用于显示两组数据之间的相关性,其中一组应包含分类或有序数据(升序或降序)。折线图最常见应用是时间序列,其中有序集由时间点组成。如果使用任意类别,应能够与它们建立某种类型的连接序列(例如,步骤的有序序列)。

折线图显示估计值。将数据集相关联的点通过代表估计值的直线或曲线连接起来。折线图可以用来预测中间值并揭示趋势。

创建简单的折线图

就像柱状图一样,你需要加载 Chart.js JavaScript 库,在页面的 <body> 中放置一个 <canvas> 对象,并创建一个新的图表,引用画布的 ID 和包含图表数据的对象。chart 对象应指定 line 作为图表类型。以下代码是创建折线图所需的最小代码,使用 Chart.js 提供的全局默认值:

<html>
 <head>
     <script src="img/Chart.min.js"></script>
 </head>
 <body>

 <canvas id="my-line-chart" width="400" height="200"></canvas>

 <script>
     const values =
       [1.17,1.35,1.3,1.09,0.93,0.76,0.83,0.98,0.87,0.89,0.93,0.81];
     const labels =

["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"];

     const dataObj = {
         labels: labels,
         datasets: [{ data: values }]
     }
     const chartObj = {
         type: "line",
         data: dataObj
     };
     new Chart("my-line-chart", chartObj);
 </script>
 </body></html>

这份数据包含来自 NASA 的 2016 年全球平均温度,结果如下所示。正如你所见,默认的折线图有灰色线条和灰色填充。你可以使用 optionsdataset 配置来更改这些默认值。完整代码在 LineArea/line-1.html

简单的折线图,显示默认的 Chart.js 属性,2016 年全球平均温度(代码:LineArea/line-1.html)

数据集配置

可以应用特定于数据集的选项来控制属性,例如每条线的颜色和宽度。向数据集添加 borderColor 属性将设置线的颜色(以及图例框),如下所示:

let dataObj = {
     labels: labels,
     datasets: [{
         data: values,
         borderColor: 'hsla(300,100%,50%,1)'
         backgroundColor: 'transparent';
     }]
 }

以下图表显示了为数据集设置 borderColorbackgroundColor 的效果。此配置仅影响一个数据集。您还可以配置影响所有数据集的属性。在此示例中,图例也通过选项配置(在单独的部分中解释)被移除。您可以在 LineArea/line-2.html 中查看完整的代码,如下面的截图所示:

简单的折线图,展示了 2016 年测量的平均全球温度(代码:LineArea/line-2.html)

以下列出的数据集属性可以用于 datasets 数组中的每个对象。许多属性也与其他显示数值数据的图表共享,例如雷达图、散点图和气泡图:

属性 描述
data Number[] 包含要显示的数据的数字数组(必填)。
label String 数据集的标签(出现在图例和工具提示中)。
backgroundColor CSS 颜色属性值字符串 在线(或上方)下的填充颜色。位置取决于 fill 属性。
borderColor CSS 颜色属性值字符串 线的颜色。
borderWidth Number 线的像素宽度。
borderDash Number[] 画布 setLineDash 方法。描述交替线宽和空间的数组。例如,[5, 10] 将创建一个具有 5 像素划线和 10 像素空间的虚线。
borderDashOffset Number 画布 lineDashOffset 属性。线段的偏移量。如果为零(默认),则 [10,10] 划线将从 10 像素线开始。如果 10,它将从 10 像素空间开始。如果 5,它将从 5 像素线开始,然后是 10 像素空间,10 像素线,以此类推。
borderJoinStyle 'bevel''round',或 'miter'(默认) 画布 lineJoin 属性。
borderCapStyle 'butt'(默认),'round',或 'square' 画布 lineCap 属性。
pointBackgroundColor ColorColor[] 点的背景颜色。
pointBorderColor ColorColor[] 点的边框颜色。
pointBorderWidth NumberNumber[] 点的边框宽度。
pointRadius StringString[] 点的半径。
pointStyle circle(默认),crosslinecrossRotdashrectRoundedrectRotstar,或 triangle 点的样式。一个字符串或指向 Image 对象的 DOM 引用。
pointHoverBackgroundColor ColorColor[] 鼠标悬停时点的背景颜色。
pointHoverBorderColor ColorColor[] 鼠标悬停时点的边框颜色。
pointHoverBorderWidth NumberNumber[] 鼠标悬停时点的宽度。
pointHoverRadius NumberNumber[] 鼠标悬停时点的半径。
pointHitRadius NumberNumber[] 点的不可见半径,该点对鼠标悬停做出反应(以显示工具提示)。
cubicInterpolationMode 'default''monotone' 默认算法采用立方加权插值。它不保证单调性(因此,如果值增加或减少,默认算法可能会偏离此行为)。
lineTension Number 立方贝塞尔线张力(这仅适用于默认插值模式)。如果为零,则图表将绘制直线。
fill falsestartendorigin 或数据集索引(相对或绝对) 此属性描述了线条之间的空间填充方式。false 关闭此功能。start 填充线条上方或之前的空间,end 填充相反的空间,origin 填充到图表的原点,索引值填充两个数据集之间的空间。一个数字表示数据集的绝对索引。包含有符号数字的字符串(例如:+2)表示相对数据集(例如:前两个数据集)。
spanGaps 'bottom''left''top''right' 如果为 false,则空值或 NaN(非零)将导致线条中断。默认为 false
showLine Boolean 如果为 false,则不会显示此数据集的行(仅显示点)。
steppedLine true = 'before'false(默认),或 'after' 以一系列步骤绘制线条。如果为 truebefore,则使用初始点。如果为 after,则使用最终值。默认为 false,这将禁用此算法。
yAxisIDxAxisID 轴 ID(见 第七章* 高级 Chart.js*) 这用于轴配置。

折线图的数据集属性

数据点代表数据集中的实际数据,并作为工具提示的锚点。可以通过 pointStyle 属性选择几种不同的数据点样式。如下所示:

可用于折线图的数据点样式(代码:LineArea/line-3-pointStyle.html)

数据点还可以配置半径、背景颜色、边框颜色和工具提示行为。在折线图中,只有点代表实际值。线条只是估计值。根据您显示的数据类型或要显示的点数,可能没有必要显示它们。您可能还想以不同的方式渲染它们。

有几个选项可以控制绘制线条的算法。lineTension 属性是一个数字(通常在 00.5 之间),它配置每条线的立方贝塞尔插值,在每两点之间绘制平滑曲线。如果将其设置为零,则图表将绘制直线,如下面的图例所示:

比较 lineTension 属性的不同值(代码:LineArea/line-4-tension.html)

如果您正在绘制离散值,可能更喜欢将线条绘制为 阶梯状。您可以通过选择 steppedLine 属性的 beforeafter 策略,根据线段的第一个或第二个点来放置阶梯。效果如下所示:

使用不同策略对 steppedLine 属性的影响(代码:LineArea/line-5-stepped.html)

折线图的选项配置

我们用于条形图的相同通用选项也可以用于配置折线图,但也有一些图表特定的选项。所有图表都预配置了默认值,可以使用本地或全局属性来覆盖这些默认值。移除使用线条的所有图表的阴影(而不是按数据集设置透明度)的一种方法是将线条元素的填充全局属性声明为 false,如下所示:

Chart.defaults.global.elements.line.fill = false;

然而,您可以通过在选项配置对象中设置每个图表的属性来配置选项。我们改进了第一个折线图的渲染方式,移除了图例,因为只有一个数据集,所以不需要图例(见 LineArea/line-2.html),如下所示:

let chartObj = {
     type: "line",
     data: dataObj,
     options:{
         legend: {
             display: false
         }
     }
 };
 new Chart("my-line-chart", chartObj);

通过将每个数据集的 pointRadius 设置为零,可以完全隐藏数据点。然而,您也可以通过设置 Chart.defaults.global.elements.point.radius 中属性的值来为所有数据集和图表全局配置它们。这将隐藏使用点的所有图表中的所有点,如下所示:

Chart.defaults.global.elements.point.radius = 0;

如果您有很多点,可能不想绘制线条。要隐藏特定数据集的线条,可以将它的 showLine 属性设置为 false,但也可以使用以下列出的选项属性配置所有线条的绘制。

它们可以设置为当前图表的本地值或所有图表的全局值:

属性 描述
showLines truefalse 如果此属性为 false,则不绘制点之间的线条。折线图的默认值为 true。散点图的默认值为 false
spanGaps truefalse 如果此属性为 false,则 null 值或 NaN(非零)会导致线条中断。默认值为 false

折线图的配置属性

使用 Chart.defaults.line 对象配置线元素的全球选项。要默认隐藏所有线条,请使用以下代码:

Chart.defaults.line.showLines = false;

本地定义在 options 对象内部。您可以使用以下代码覆盖图表的默认值:

options: { showLines: true }

包含多个数据集的折线图

每个数据集通过单独的线条在折线图中显示。以下示例向我们的图表添加了一组新的值,即 1880 年测量的平均月温度。现在我们可以将两个数据集绘制在同一个网格中,并比较它们与 2016 年的平均温度,如下所示:

// NASA/GISS Temperature anomalies from 1880 to 2016
 let values2016 =
   [1.17,1.35,1.3,1.09,0.93,0.76,0.83,0.98,0.87,0.89,0.93,0.81];
 let values1880 =
   [-0.3,-0.21,-0.18,-0.27,-0.14,-0.29,-0.24,-0.08,-0.17,-0.16,-0.19,
   -0.22];
 Chart.defaults.global.elements.line.fill = false;

 let labels =
   ["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov",
   "Dec"];

 let dataObj = {
     labels: labels,
     datasets: [{
         label: '2016',
         data: values2016,
         borderColor: 'hsla(300,100%,50%,1)',
         borderDash: [5, 5],
     },{
         label: '1880',
         data: values1880,
         borderColor: 'hsla(200,100%,50%,1)'
     }]
 }
// the rest of the code is identical

前面代码的结果显示在以下图表中。完整代码位于LineArea/line-6-datasets.html。该图表显示,2016 年的平均温度异常值比 1880 年的测量值高约 1°C:

图片

包含两个数据集的折线图(代码:LineArea/line-6-datasets.html

可以使用fill属性与布尔值一起使用来打开/关闭所有线条的阴影,但它也可以用作dataset属性来为单个数据集配置阴影策略。在这种情况下,它接收一个标识轴线的字符串:'start''end''origin',这将使图表在线条和轴线之间(最小、最大或零轴线)着色。它也可以在线条之间着色,指定一个相对数字作为字符串:'-1'将在当前数据集和上一个数据集之间着色,'+2'将从当前数据集着色到dataset数组中高两个位置的数据集。您还可以引用dataset数组的绝对索引。以下图表比较了这些填充策略的一些效果:

图片

折线图填充策略(代码:LineArea/line-7-fill.html

从外部文件加载数据

折线图非常适合揭示大量数据中的趋势和关系。公共数据通常以 CSV 和 JSON 等标准格式广泛可用,但在使用之前通常需要降级、解析并转换为 Chart.js 期望的数据格式。在本节中,我们将从公共数据文件中提取数据并将其转换为揭示趋势的可视化。

对于所有使用外部文件的示例,您需要使用 Web 服务器打开您的文件。双击 HTML 文件并在浏览器中打开它不会起作用。如果您没有使用 Web 服务器运行文件,请参阅第二章中“技术基础”部分的加载数据文件部分,了解如何配置 Web 服务器进行测试。

之前示例中的温度数据是从 NASA 戈达德太空研究所GISS)网站(data.giss.nasa.gov/gistemp)获取的 JSON 文件中提取的,该网站包含了 1880 年至 2016 年之间每年的月度测量数据。将所有月份的数据绘制在一张图表上将会非常有趣。我们可以通过加载文件并使用 JavaScript 提取所需的数据来实现这一点。

以下是从 GISS 网站 JSON 文件中提取的片段。它也可以在 GitHub 上本章的存储库中找到,位于Data/monthly_json.json

[
   {"Date": "2016-12-27", "Mean": 0.7895,  "Source": "GCAG"},
   {"Date": "2016-12-27", "Mean": 0.81, "Source": "GISTEMP"},
   {"Date": "2016-11-27", "Mean": 0.7504,  "Source": "GCAG"},
   {"Date": "2016-11-27", "Mean": 0.93, "Source": "GISTEMP"},
   {"Date": "2016-10-27", "Mean": 0.7292,  "Source": "GCAG"},
   {"Date": "2016-10-27", "Mean": 0.89, "Source": "GISTEMP"},
   /* ... many, many more lines ... */
   {"Date": "1880-02-27", "Mean": -0.1229,  "Source": "GCAG"},
   {"Date": "1880-02-27", "Mean": -0.21, "Source": "GISTEMP"},
   {"Date": "1880-01-27", "Mean": 0.0009,   "Source": "GCAG"},
   {"Date": "1880-01-27", "Mean": -0.3,  "Source": "GISTEMP"}
 ]

文件应该异步加载。你可以使用任何 Ajax 库来完成此操作(例如,JQuery)或使用所有现代浏览器都支持的标准的 ES2015 功能。在这本书中,我们将使用标准的 JavaScript fetch()命令(在 GitHub 仓库中,大多数示例也有 JQuery 替代方案)。

fetch()命令是响应式的。它将等待整个文件加载到内存中,然后才移动到第一个then()步骤,该步骤处理响应并提取 JSON 字符串(使用text()方法)。第二个then()步骤仅在所有内容都放置在字符串中,并在最终步骤中可用于解析之后才开始,如下所示:

fetch('monthly_json.json') 
    .then(response => response.text())
    .then((json) => {
        const dataMap = new Map();
    ...
    });

在使用 JSON 文件(它是一个字符串)之前,我们需要解析它,使其成为一个 JavaScript 对象,然后我们可以使用点操作符读取单个字段。这可以通过标准的 JavaScript 命令JSON.parse()完成,如下所示:

const obj = JSON.parse(json);

如果你使用fetch()而不是 JQuery 或其他库,你可能更喜欢使用一个加载并解析 JSON 的函数。在这种情况下,你不应该运行前面的命令。

数据包含两个测量值,分别标记为GCACGISTEMP。我们只需要其中一个,因此我们将仅过滤具有GISTEMP作为Source的对象。我们还将反转数组,以便较早的测量值在图表中首先出现。我们可以在一行中完成所有这些,如下所示:

const obj = JSON.parse(json).reverse()
                            .filter(field => field.Source == 'GISTEMP');
console.log(obj);

最后一条将在你的浏览器 JavaScript 控制台中打印以下代码:

 Array(1644)
  [0 ... 99]
 0:{Date: "1880-01-27", Mean: -0.3, Source: "GISTEMP"}
 1:{Date: "1880-02-27", Mean: -0.21, Source: "GISTEMP"}
 2:{Date: "1880-03-27", Mean: -0.18, Source: "GISTEMP"}
 3:{Date: "1880-04-27", Mean: -0.27, Source: "GISTEMP"}
 …

现在,选择我们需要的用于构建每年数据集的数据变得容易了。最好的方法是创建一个Map,存储每个值和月份,并使用年份作为检索键。将日期组件拆分以提取年份和月份,然后将这些值和温度异常存储在一个新的对象(具有yearmonthvalue属性)中,为每个Map条目。

这些步骤在以下代码中执行:

const dataMap = new Map();
obj.forEach(d => {
    const year = d.Date.split("-")[0], month = d.Date.split("-")[1];
    if(dataMap.get(year)) {
        dataMap.get(year).push({year: year, month: month, value: d.Mean});
     } else {
        dataMap.set(year, [{year: year, month: month, value: d.Mean}]);
     }
 });
console.log(dataMap); // check the structure of the generated map!
draw(dataMap);

生成的地图将包含数据集中每一年份的一个键。每个条目的值将是一个包含 12 个对象的数组,每个对象代表一个月。使用浏览器中的 JavaScript 控制台来检查生成的地图。

draw()函数将dataMap转换为 Chart.js 可以使用的形式。对于每个条目,它将创建一个dataset对象并将其添加到datasets数组中。每个dataset对象包含一个具有数据值数组的data属性(每月一个),以及数据集配置属性,如线条颜色和标签。地图的键(年份)是标签,颜色使用渐变序列生成,根据年份改变色调,如下所示:

function draw(dataMap) {
    const datasets = [];
    dataMap.forEach((entry, key) => {
        const dataset = {
            label: key, // the year
            data: entry.map(n => n.value), 
            // array w temperature for each month
            borderColor:     'hsla('+(key*2)+',50%,50%,0.9)', 
            //gradient
            backgroundColor: 'hsla('+(key*2)+',50%,50%,0.9)',
            borderWidth: 1,
            pointRadius: 0 // hide the data points
         };
         datasets.push(dataset);
     });
 ...

现在我们可以组装数据对象并实例化折线图,如下所示:

    const months = ["Jan","Feb", ...,"Oct","Nov","Dec"];
    Chart.defaults.global.elements.line.fill = false;
    const chartObj = {
        type: "line",
        data: {
            labels: months,
            datasets: datasets
        }
    };
    new Chart("my-line-chart", chartObj);
 }

最终结果如下所示。完整的代码可在LineArea/line-8-load-fetch.html(fetch 版本)和LineArea/line-8-load-jquery.html(JQuery 版本)中找到:

使用外部数据绘制的折线图,显示从 1880 年到 2016 年的温度变化(数据:NASA/GISS;代码:LineArea/line-8-load-fetch.html 用于获取版本,或 LineArea/line-8-load-jquery.html 用于 jQuery 版本)

看起来不错,但信息太多。我们可以过滤掉一些结果,但也可以简单地减少标签的数量。options.legend.labels.filter属性支持一个callback函数,我们可以使用它来过滤掉选定的标签。在下面的代码中,它将只显示间隔 20 年的标签:

const chartObj = {
    type: "line",
    data: {
        labels: labels,
        datasets: datasets
    }
    options:{
        legend: {
            labels: {
                filter: function(item, chart) {
                    return new Number(item.text) % 20 == 0;
                }
            }
        }
    }
};

结果如下所示,完整代码在LineArea/line-10-filter.html中。现在只显示了几个图例,颜色差异足够大,可以与图表的不同部分相关联。尽管图表中仍然有大量信息,但颜色足以揭示温度上升的趋势:

图片

使用外部数据过滤掉多余标签后的折线图(代码:LineArea/line-10-filter.html)

堆叠面积图

可以使用折线图来显示每个国家每年向大气中排放的二氧化碳量。这将揭示一个国家的排放量是增加、稳定还是减少,但这样的折线图对于显示排放到空气中的二氧化碳量以及每个国家对此总量的贡献并不是非常有用。你可以使用堆叠面积图来显示这类信息。

Chart.js 中没有特殊的area类型图表。相反,你可以通过配置每个数据集在折线图中的填充属性来创建一个简单的重叠面积图。要创建堆叠面积图,你需要将 x 轴和 y 轴的堆叠属性设置为true

让我们尝试一个例子。我们将使用一个包含 1960 年至 2014 年选定国家碳排放(以千吨为单位)数据的 JSON 文件。它基于包含所有国家数据的 CSV 文件,该文件可以从世界银行公共数据库中下载。我创建了一个只包含六个最大污染者的 JSON 版本文件,将其他所有国家的数据合并到一个条目中。这是我们将会使用的文件(Data/world_bank_co2_kt.json),如下所示:

{ "labels":[1960,1961,…,2013,2014],
   "entries":[
      {"country":"Others",
       "data":[983835.74025,1015886.52639,
        …,10073290.7688,10300830.9827]},
      {"country":"Russian Federation",
       "data":[0,0,… ,1778561.006,1705345.684]},
      {"country":"India",
       "data":[120581.961,130402.187,… ,2034752.294,2238377.137]},
      {"country":"Japan",
       "data":[232781.16,283118.069,… ,1246515.976,1214048.358]},
      {"country":"China",
       "data":[780726.302,552066.85,… ,10258007.128,10291926.878]},
      {"country":"European Union",
       "data":[2359594.88616257,2445945.66448806,… 
        ,3421472.348,3241844.353]},
      {"country":"United States",
       "data":[2890696.1,2880505.507,2987207.873,… 
        ,5159160.972,5254279.285]}
 ]}

如前例所示,我们需要加载文件并解析 JSON 字符串,如下所示:

fetch('world_bank_co2_kt.json')
         .then(response => response.text())
         .then((json) => {
     draw(JSON.parse(json));
 });

下一步是设置一个从数据中获取的labelsdatasets数组。JSON 文件已经包含了一个包含年份的数组,所以你只需要直接将其复制到图表的数据对象的labels属性中。datasets数组是通过遍历数据文件条目数组中的每个条目来组装的,从中提取数据集的标签(来自country属性)和数据数组(来自data属性)。我们将使用数组的索引来生成不同的颜色,如下所示:

 function draw(datasetsObj) {
     const datasets = [];
     datasetsObj.entries.forEach((entry, index) => {
         const color = 'hsla('+(index+5)*50+',75%,75%,1)';
         const dataset = {
             label: entry.country,
             data: entry.data,
             borderColor: color,
             backgroundColor: color,
             borderWidth: 3,
             fill: 'start', // fills the space below each line
             pointRadius: 0
         };
         datasets.push(dataset);
     });

     const dataObj = {
         labels: datasetsObj.labels, // copied from the JSON data
         datasets: datasets
     }

     new Chart("my-area-chart", {type: "line", data: dataObj });

以下代码的结果如下。完整的代码在LineArea/line-11-area.html中。1990 年和 1992 年之间的步骤是由前几年数据不足造成的,主要来自华沙条约国家和苏联:

图表示例

展示重叠(非堆叠)数据集的面积图(代码:LineArea/line-11-area.html)

Chart可能不是你预期的样子。它并没有堆叠数据。其他数据集覆盖了所有其他数据集。

数据集可以以两种方式堆叠:在x轴上,或者在y轴上,因此你必须告诉 Chart.js 你想要如何操作。在这个例子中,将年份相加没有意义,但将碳排放量相加是有意义的,因此我们必须堆叠y轴。这是通过将选项配置对象中的scales.yAxes[0].stacked属性设置为true来实现的,如下所示:

 const chartObj = {
     type: "line",
     data: dataObj,
     options:{
        scales: {
            yAxes: [{
               stacked: true
            }]
       },
           legend: {
            labels: {
                boxWidth: 20,
            }
         }
     }
 };

在前面的选项配置中,我们还减小了图例框的大小到一半(boxWidth属性)。你可以看到最终结果如下。完整的代码在LineArea/line-12-area-stacked.html中:

图表示例

展示总排放量和按国家排放的 CO2 排放量的堆叠面积图(代码:LineArea/line-12-area-stacked.html)

现在图表显示,从 1990 年到 1992 年的步骤主要归因于俄罗斯,因为世界银行在 1990 年之前没有俄罗斯(当时是苏联)的碳排放数据。

雷达图

雷达图是在径向轴上绘制的折线图。它们可以与包含至少三个值的单个或多个数据集一起使用。只有一个轴,它从中心开始。每条线从同一点开始和结束,因此雷达图通常用于显示具有循环性质(例如小时、月份、日程安排或重复事件)的值,一个以相同位置开始和结束的类别顺序列表(例如往返),或者没有特定顺序的类别。雷达图可以通过揭示强项和弱项,或显示数据的异常值和共性来比较不同的数据集。它通常与少量数据集(即不超过三到四个)配合使用效果最好。

雷达图通常不适合大型数据集。在这些情况下,通常更好的选择是使用笛卡尔线图或条形图。径向距离也较难感知,尽管可以通过网格最小化这种限制。

雷达图的配置属性与线图相同。你甚至可以重用相同的 datasets 和标签。每个数据集的数据属性必须包含一个数字数组,并且chart对象应该配置为type='radar'

在以下示例中,雷达图被用来比较为期 30 天的旅行三种不同的行程安排。每个数据集列出了在每座城市花费的天数。使用这个图表,游客可以快速可视化旅行中每天将如何分配到每个城市,这使得选择最佳行程变得更容易:

    let dataObj = {
         labels: ["Lisbon", "Paris", "Berlin", "Moscow", "Rome", 
         "Barcelona"],
         datasets: [
             {
                 label: "Trip schedule #1",
                 data: [5,5,5,5,5,5],
                 borderColor: 'red',
                 backgroundColor: 'hsla(0,75%,75%,.25)'
             },{
                 label: "Trip schedule #2",
                 data: [7,3,3,3,7,7],
                 borderColor: 'blue',
                 backgroundColor: 'hsla(240,75%,75%,.25)'
             },{
                 label: "Trip schedule #3",
                 data: [4,7,7,7,3,2],
                 borderColor: 'yellow',
                 backgroundColor: 'hsla(60,75%,75%,.25)'
             }
         ]
     }

     const chartObj = {
         type: "radar",
         data: dataObj,
         options: {
             scale: {
                 ticks: {
                     beginAtZero: true,
                     stepSize: 1 // show one gridline per day
                 }
             }
         }
     };
     new Chart("my-radar-chart", chartObj);

雷达图没有包含x轴和y轴的scales属性,而只有一个scale属性。网格结构在ticks属性中配置(关于刻度的更多信息请参阅本章末尾)。

结果如下所示。你可以在Radar/radar-1.html中查看完整的代码:

比较为期 30 天的旅行三种不同行程安排的雷达图(代码:Radar/radar-1.html)

雷达图非常适合周期性数据,例如一年中的月份。让我们尝试将上一节中创建的笛卡尔线图转换为具有相同数据的雷达图。大部分代码都是相同的。你只需要更改图表类型,但一些细微的配置更改会使它看起来更好。

以下代码展示了略微修改后的draw()函数,它使用相同的 NASA/GISS 月度温度数据,但在雷达图中绘制线条:

 const months = ["Jan", "Feb", "Mar", ... , "Sep", "Oct", "Nov", 
 "Dec"];

 function draw(datasetMap) {
     const datasets = [];

     datasetMap.forEach((entry, key) => {
         const dataset = {
             label: key,
             data: entry.map(n => n.value),
             borderColor:     'hsla('+(key*2)+',50%,50%,.9)',
             backgroundColor: 'hsla('+(key*2)+',50%,50%,0.1)',
             borderWidth: 1,
             pointRadius: 0, // don't show the data points
             lineTension: .4 // do draw lines as curves (not default in 
                              radar)
         };
         datasets.push(dataset);
     });

     const dataObj = {
         labels: months,
         datasets: datasets
     }

     const chartObj = {
         type: "radar",
         data: dataObj,
         options: {
             animation: {
                 duration: 0
             },
             scale: {
                 ticks: {
                     max: 1.5
                 }
             },
             legend: {
                 labels: {
                     boxWidth: 20,
                     filter: function(item, chart) {
                         return new Number(item.text) % 20 == 0
                                       || item.text % 2016 == 0;
                     }
                 }
             }
         }
     };

     new Chart("my-radar-chart", chartObj);
 }

雷达图的默认线张力为0,这会绘制直线。由于这些值是平均值,我们为lineTension属性选择了介于 0 和 0.5 之间的值,以便图表绘制出曲线。

完整的代码可在Radar/radar-3.html中找到。结果如下所示:

展示从 1880 年到 2016 年全球温度上升的雷达图(代码:Radar/radar-3.html)

颜色的变化足以揭示温度逐年上升。然而,如果你希望有更高的精度,可以尝试过滤掉一些datasets,并仅显示每二十年的数据,如下所示:

datasets: datasets.filter(d => d.label % 20 == 0 || d.label % 2016 == 0)

结果仅展示八年,如下所示。完整的代码在Radar/radar-4.html中:

展示从 1880 年到 2016 年每 20 年全球温度上升的雷达图(代码:Radar/radar-4.html)

饼图和环形图

饼图和环形图用于显示数据作为整体部分之间的数值比例。每个数据值都表示为一个切片,代表一个成比例的数量。这些图表非常受欢迎,但也受到广泛的批评。由于我们不太擅长感知角度,与条形图或折线图相比,比较饼图中的数据要困难得多。使用饼图来比较非常小的数据集可以避免或减少这些问题。

饼图通常用于显示单个数据集。图表对象的 type 属性应该是 pie。环形图与饼图等效,但它们是通过 type: doughnut 创建的。您还可以通过简单地更改数据集属性 cutoutPercentage50(或任何非零值)将任何饼图转换为环形图。

创建一个简单的饼图

让我们创建一个简单的饼图来比较世界最大污染者在某一年内的二氧化碳排放量。您可以使用我们用于区域图的相同数据,但您需要选择一个数据集,将国家名称放在 labels 数组中,将一年的数据放在 data 数组中,并为每个切片生成颜色。所有这些都可以在 JavaScript 中完成(见 Pie/pie-2-fetch.html),但为了简单起见,并专注于简单饼图的构建,我们将直接在 HTML 文件中包含数据,如下面的代码块所示:

 const dataset = [1.21, 1.71, 2.24, 3.24, 5.25, 10.29, 10.3]; // 2014 data
 const labels = ["Japan", "Russian Federation", "India", "European Union",
                 "United States", "China", "Others"];
 const colors = [];

 dataset.forEach((entry, index) => { // generate some colors
     colors.push('hsla('+((index+5)*50)+',75%,75%,1)');
 });

datasets 数组包含一个数据集,如下所示:

const dataObj = {
     labels: labels,
     datasets: [{
         data: dataset,
         backgroundColor: colors,
         borderWidth: 3
     }]
 }

图表类型应该是 pie,如下所示:

const chartObj = {
     type: "pie",
     data: dataObj,
     options:{
         title: {
             text: "CO2 emissions (billions of tonnes)",
             display: true,
             fontSize: 24
         },
         legend: {
             labels: {
                 boxWidth: 20,
             },
             position: 'right'
         }

     }
 };
 new Chart("my-pie-chart", chartObj);

结果如下所示。您还可以在 Pie/pie-1.html 中查看完整的代码。请注意,切片没有标签。只有当您将鼠标悬停在切片上时,您才能看到每个切片的值。它将在工具提示中显示:

一个简单的饼图,显示了最大的污染者按百万吨计的二氧化碳排放量(代码:Pie/pie-1.html)

饼图的数据集属性

除了 datalabels 属性外,还可以在每个数据集对象中使用以下列出的其他属性来配置每个切片的颜色和样式。所有属性都接受一个属性数组,并且每个属性都应用于相应的切片:

属性 描述
backgroundColor CSS 颜色字符串数组 切片的填充颜色
borderColor CSS 颜色字符串数组 切片的边框颜色
borderWidth 数字数组 切片的边框宽度
hoverBackgroundColor CSS 颜色字符串数组 鼠标悬停时切片的填充颜色
hoverBorderColor CSS 颜色字符串数组 鼠标悬停时切片的边框颜色
hoverBorderWidth 数字数组 鼠标悬停时切片的边框宽度

饼图和环形图的数据集选项

配置选项

饼图的常见配置选项是继承的,但也有一些特定于饼图和环形图的选项。这些选项如下表所示:

属性 描述
cutoutPercentage 数字。默认值:对于 'pie'0,对于 'doughnut''50' 从图表中间切掉的部分的百分比
rotation 数字。默认值:-0.5 * Math.PI 绘制弧线的起始角度
circumference Number. 默认值:*2 * Math.PI* 饼图的周长

饼图和环形图的配置选项

这些选项将合并(并覆盖)全局配置选项。也可以通过Chart.defaults.doughnutChart.defaults.pie对象设置每种图表类型的默认选项,它们支持前面列出的相同属性。

如何在饼图的切片中显示值

目前,没有原生的 Chart.js 方法在饼图中显示值或百分比而不使用工具提示。但你可以通过插件或扩展来实现这一点。在以下示例中,我们将使用一个非常简单的库,称为Chart.Piecelabel.js。你可以从gihub.com/emn178/Chart.PieceLabel.js下载它,并使用脚本标签将其包含在你的页面中:

<script src="img/Chart.PieceLabel.js"></script>

就这样!现在你可以将pieceLabel属性添加到options对象中,并配置切片的标签。你可以显示绝对值或百分比,将标签放置在切片内部、边缘或外部,在弧上绘制文本,并配置多个字体属性。以下是一些这些属性的示例(你可以查看库的文档以获取更多属性):

属性 描述
render 'percentage'(默认)或 'value' 显示切片的百分比或值。
precision 数字 百分比(小数点后的数字)的precision(不适用于其他值)。
fontSize, fontColor, fontSize, 和 fontFamily CSS 属性值字符串 更改标签的字体属性。
textShadow truefalse 将阴影应用于标签(阴影属性,如偏移量和颜色,也可以通过其他属性进行配置)。
position 'default', 'border''outside' 将标签放置在不同的位置。
arc truefalse 绘制与弧对齐的文本。当位置为'outside'时效果更好。

Chart.PieceLabel.js插件的某些配置选项

要在上一示例中创建的饼图中包含标签,请将以下属性添加到你的图表的options对象中(见Pie/pie-3-plugin.html):

options:{
     pieceLabel: {
        fontColor: '#000',
        render: 'value',
        textShadow: true,
        position: 'border'
    },
 …

运行脚本,你将得到包含每个切片所表示的值的标签,如下面的图表所示:

图片

使用 Chart.PieceLabel.js 扩展显示每个切片上的标签的简单饼图(代码:Pie/pie-3-plugin.html)

如果你想要更多的定制,你可以尝试其他插件,如ChartJS-DatalabelsChartJS-Outlabels,它们支持复杂的标签放置选项。这两个插件将在第六章,配置样式和交互性中探讨。

准备饼图和环形图的图表数据

饼图不能用于任何类型的数据。它们应该仅用于显示部分与整体的关系,并且包含的数据值不应超过六七个。以下截图显示了当你创建包含过多数据的饼图或环形图时会发生什么。在这个例子中,我们加载了一个包含近 200 个国家人口的环形图。它可能是一种艺术,但并不是真正有用的可视化:

滥用饼图(代码:Pie/pie-4-evilpie.html;另见 Pie/pie-6-evilpie.html)

即使将此数据集减少到不到 20 个值,它仍然不能有效地在饼图中显示。颜色不够,而且很难在这么多切片中放置标签,更不用说角度比较起来要困难得多。在这种情况下,你应该切换到条形图,它可以有效地比较 20 个值甚至更多。

以下截图显示了使用相同数据创建的条形图,过滤以显示 35 个人口最多的国家:

与饼图相比,条形图更适合比较大型数据集(代码:Pie/pie-5-evilpie-as-bar.html;另见 Pie/pie-7-evilpie-as-bar.html)

如果你仍然想使用饼图,你需要减少数据样本,但仅仅过滤数据(例如,只包括人口最多的国家)是不够的。因为饼图应该显示部分与整体的关系,但你还需要添加被排除的项目(例如,将小国的人口加起来,就像在二氧化碳排放的例子中那样)。

在以下示例中执行此操作:它加载并解析 CSV 数据文件,按人口排序数据,创建包含最大国家的对象数组,最后将所有其他人口加到新的 others 条目中。

解析 CSV 时,我们使用流行的 PapaParse 库 (github.com/mholt/PapaParse). 你可以使用以下代码将其包含在你的页面中:

<script   
src="img/papaparse.min.js">
 </script>

PapaParse 读取 CSV 并将数据转换为 JavaScript 数组,其中每行都是一个对象,列标题作为键。要从任何包含标题的第一行(这是最常见的情况)的 CSV 文件中获取数据,请使用以下代码:

const data = Papa.parse(csv, {header: true}).data;

现在,对于每个数组项,你可以使用 item.headeritem['header'] 访问值。

以下代码加载 CSV,解析它,并调用一个函数来减少数据。然后,减少后的数据传递给 drawData() 函数,该函数将使用 Chart.js 绘制饼图,如下所示:

 const numberOfEntries = 6; // change this to include more countries

 fetch('../Data/WPP2017_UN.csv')
     .then(response => response.text())
     .then((csv) => {
         const data = Papa.parse(csv, {header: true}).data;
         const reduced = reduceData(data);
         drawData(reduced);
 });

reduceData() 函数通过按人口排序并切片数组来过滤人口最多的国家,并将剩余国家的人口添加到 others 条目中,如下所示:

 function reduceData(array) {
     array.sort((a, b) =>  a["2015"] - b["2015"]);

     const topEntries =
         array.slice(array.length - numberOfEntries,array.length)
              .map(d => ({country: d["Country or region"], data: 
              +d["2015"]}));

     let others = array.slice(0, array.length - numberOfEntries);
     const sumOthers = others.map(d => +d["2015"]).reduce((a,b) => a+b, 0);
     others = {country: "Others", data: sumOthers};
     topEntries.push(others);
     return topEntries;
 }

drawData() 函数准备数据以便 Chart.js 使用,并使用结果填充图表的标签、datasets[0].datadatasets[0].backgroundColor,如下所示:

 function drawData(data) {
     const dataset = [], labels = [], colors = [];
     let count = 0;
     data.forEach(d => {
         dataset.push(Math.round(d.data/1000));
         labels.push(d.country);
         colors.push('hsla('+(count++ * 
         300/numberOfEntries)+',100%,70%,.9)');
     });

     const dataObj = {
         labels: labels,
         datasets: [
             {
                 data: dataset,
                 backgroundColor: colors,
                 borderWidth: 5,
                 hoverBackgroundColor: 'black',
                 hoverBorderColor: 'white'
             }
         ]
     }

     const chartObj = {
         type: "doughnut",
         data: dataObj,
         options: {
             title: {
                 display: true,
                 position: 'left',
                 fontSize: 24,
                 text: "World population (millions)"
             },
             legend: {
                 position: 'right'
             },
             pieceLabel: {
                 fontColor: '#000',
                 render: 'value',
                 textShadow: true,
                 position: 'border'
             }
         }
     };
     new Chart("my-pie-chart", chartObj);
 }

您可以在 Pie/pie-8-filter.html 中查看完整代码。以下结果图表显示了六个国家与其他所有国家的比较:

一个比较人口最多的国家和世界上其他国家的环图(代码:Pie/pie-8-filter.html)

改变圆周

饼图和环图不必是完整的圆。您可以设置 circumference 属性的值(以弧度为单位)并使用小于 360 度的值(2 * Math.PI 弧度)。使用 Math.PI 作为圆周,您可以得到半饼图或半环图。这些图表适合比较两个或三个值,并且可能更适合在较小的空间中使用。

在以下示例中,我们修改了之前的图表。它使用半饼图比较了中国和印度的 2017 年人口与世界上其他国家的人口。以下片段包含相关代码。您可以在 Pie/pie-9-halfpie.html 中查看完整代码:

 const numberOfEntries = 2;
 // ...
 const chartObj = {
     type: "doughnut",
     data: dataObj,
     options: {
         circumference: Math.PI, // creates the half-pie
         rotation: Math.PI / 2,  // rotates the half-pie 180 degrees
         title: {...},
         legend: {...},
         pieceLabel: {...}
     }
 };
new Chart("my-pie-chart", chartObj);

结果图表如下所示。rotation 属性不包含旋转量,而是一个位置(即绘制弧线的起始角度),默认旋转位置是 -Math.PI/2,因此 Math.PI/2 的值实际上旋转了 180 度,而不是 90 度,这可能与预期不同(为了达到垂直位置,请使用 Math.PI):

改变饼图的圆周和旋转属性(代码:Pie/pie-9-halfpie.html)

包含多个数据集的饼图和环图

通常,您只使用饼图显示单个数据集,但支持多个数据集。它们显示为同心圆。在这种情况下,标记数据是强制性的,因为无法直观地比较切片的大小。

以下示例使用包含 1980 年和 2015 年国家人口估计的两个数据集来创建饼图,1980 年的值在内部圆圈中,2015 年的值在外部圆圈中。以下代码片段如下所示。您可以在 Pie/pie-10-multiset.html 中查看完整代码:

 const dataset2015 = [189,206,258,320,1309,1397,3703],
       dataset1980 = [78,121,147,230,697,994,2191];

 const labels = ["Pakistan", "Brazil", "Indonesia", "United States of 
                America", "India", "China", "Others"];

 const colors2015 = [], colors1980 = [];

 let count = 0;
 labels.forEach(d => {
     count++;
     colors2015.push('hsla('+(count * 300 / labels.length)+', 100%,
     50%, .9)');
     colors1980.push('hsla('+(count * 300 / labels.length)+', 100%,
     75%, .9)');
 });

 const dataObj = {
     labels: labels,
     datasets: [
         { data: dataset2015, backgroundColor: colors2015 },
         { data: dataset1980, backgroundColor: colors1980 }
     ]
 }
 const canvas = document.getElementById("my-pie-chart");
 const ctx = canvas.getContext("2d");

 const chartObj = {
     type: "doughnut",
     data: dataObj,
     options: {
         animation: { // to draw on canvas use this callback
             onComplete: function() {
                 ctx.fillText("Population in 1980",
                               canvas.width/2 - 140,canvas.height/2);
                 ctx.fillText("Population in 2015",
                               canvas.width/2 + 70,canvas.height - 10);
             }
         } // ...
     }
 };

 const chart = new Chart("my-pie-chart", chartObj);
 chart.update();

饼图和环图中心的标签是通过直接在画布上绘制创建的。如果您需要这样做,必须使用回调。当图表绘制完成时,会调用 onComplete 回调(在 options.animation 下配置)。如果您不使用回调,Chart.js 可能会擦除您所绘制的任何内容。此行为将在第六章,配置样式和

交互性

结果如下所示:

一个包含两个数据集的饼图(代码:Pie/pie-10-multiset.html)

这些图表可能视觉上吸引人,但它们可能会引入一些严重的感知错误。外弧被感知得比实际大得多。这是一种视觉错觉。在前面的图表中,人口增长不会引起注意,除非差异显著。如果您反转数据集的顺序,将 1980 年的值放在外圈上,就可以可视化这个问题。以下图表显示了这一点,其中似乎有些人口按整个比例增长,而实际上它们都在减少。这个图表在欺骗我们:

在饼图和环形图中无法比较多个数据集:较小的值看起来似乎更大(代码:Pie/pie-11-evilmultiset.html)

极坐标面积图

极坐标面积图类似于在径向轴上渲染的条形图。如果您需要精度,条形图通常是更好的选择,但您可能会选择极坐标面积图来获得其视觉效果。

要创建极坐标面积图,您需要以与条形图相同的方式设置数据,然后将类型更改为 polarArea。与雷达图一样,只有一个 scale 属性和轴需要配置。

在以下示例中,我们使用极坐标面积图来比较世界海洋的体积。它是基于我们在第三章,Chart.js – 快速入门中创建的具有相同数据的条形图。

 const labels = ["Arctic", "Southern", "North Atlantic", "South 
                Atlantic", "Indian", "South Pacific", "North Pacific"];
 const volume = [18750, 71800,146000,160000,264000,329000,341000]; 
 // km3*10³

 Chart.defaults.global.elements.rectangle.borderWidth = 1;

 const chartObj = {
     labels: labels,
     datasets: [
         {
             label: "Volume",
             data: volume,
             borderWidth: 2,
             backgroundColor: [
                 'hsla(260,100%,75%,.7',
                 'hsla(245,100%,75%,.7',
                 'hsla(230,100%,75%,.7',
                 'hsla(210,100%,75%,.7',
                 'hsla(195,100%,75%,.7',
                 'hsla(180,100%,75%,.7',
                 'hsla(165,100%,75%,.7']
         }
     ]
 }
 new Chart("my-polar-area-chart", {
     type: "polarArea",
     data: chartObj,
     options: {
         title: {
             display: true,
             position: 'left',
             fontSize: 24,
             text: "Volume of water (in 1000 cubic km)"
         },
         legend: {
             position: 'right'
         }
     }
 });

您可以在 PolarArea/polar-area-1.html 中查看完整的代码。结果如下所示:

比较每个海洋中水体积的极坐标面积图(代码:PolarArea/polar-area-1.html)

极坐标面积图与饼图和环形图共享相同的数据集属性,主要由设置切片边框宽度、边框颜色和填充颜色的属性组成。

极坐标面积图有一个特定的配置选项,如下所示:

属性 描述
startAngle 数字 开始绘制弧线的角度

极坐标面积图的配置选项

您可以绘制包含多个数据集的极坐标面积图,但它们将会重叠。当前版本的 Chart.js(2.7.2)不支持在这些图表中堆叠或其他显示多个数据集的方式。您还可以查看 PolarArea/polar-area-2.htmlpolar-area-3.html 以了解其他配置极坐标面积图的方法。

散点图和气泡图

散点图或散点图在数据科学和统计学中非常受欢迎。它们可以用来探索变量之间的各种相关性,揭示趋势、集群、线性关系和非线性关系。它是问题解决程序和决策过程中的一个基本工具。

散点图通过在笛卡尔坐标系中绘制两个变量的点来显示它们之间的相关性。可以通过使用不同的形状和/或颜色来显示额外的变量。

气泡图是一种散点图,它使用不同半径的圆来显示额外的变量。将散点图与其他图表(如折线图和柱状图)重叠也是常见的,这样可以强调模式并比较原始数据与估计值(如趋势线,最佳拟合)。

创建散点图

type属性应该是scatter。散点图支持与折线图相同的属性,但数据属性应包含一个包含以下结构的点对象数组:

{
     x: number,
     y: number
 }

以下示例创建了一个包含单个数据集的简单散点图。数据值由为x属性生成的数字序列组成,以及每个数字的正弦函数为y属性:

const dataset = [];
 for(let i = 0; i <= 360; i+= 5) {
     const point = {
         x: i,
         y: Math.sin(i * Math.PI / 180)
     }
     dataset.push(point);
 }

 const dataObj = {
     datasets: [
         {
             data: dataset,
             pointRadius: 2,
             backgroundColor: 'red'
         }
     ]
 }

 const chartObj = {
     type: "scatter",
     data: dataObj,
     options: {
         legend: {
             display: false
         },

     }
 };
 new Chart("my-scatter-chart", chartObj);

你可以在ScatterBubble/scatter-1.html中查看完整的代码。结果如下所示:

一个简单的散点图,其中 x = n,y = sin(x)(代码:ScatterBubble/scatter-1.html

可以在同一张图表上显示多个数据集。以下示例生成两个额外的数学函数,并使用相同的刻度显示它们的图形:

const dataset1 = [], dataset2 = [], dataset3 = [];
 for(let i = 0; i <= 360; i+= 5) {
     const n = i * Math.PI / 180;
     const point1 = { x: n - Math.PI, y: Math.sin(n) }
     const point2 = { x: n - Math.PI, y: Math.cos(n) }
     const point3 = { x: Math.cos(n) + Math.sin(n), y: Math.cos(n) -
     Math.sin(n) }
     dataset1.push(point1);
     dataset2.push(point2);
     dataset3.push(point3);
 }

 const dataObj = {
     datasets: [
         {   data: dataset1,
             pointRadius: 2,
             backgroundColor: 'red'
         },{
             data: dataset2,
             pointRadius: 2,
             backgroundColor: 'blue'
         },{
             data: dataset3,
             pointRadius: 2,
             backgroundColor: 'green'
         }
     ]
 }

 const chartObj = {
     type: "scatter",
     data: dataObj,
     options: {
         legend: { display: false },
         scales: {
             yAxes: [{
                 ticks: {min: -2, max: 2}
             }]
         }
     }
 };
 new Chart("my-scatter-chart", chartObj);

你可以在ScatterBubble/scatter-3.html中查看完整的代码。结果如下所示:

包含多个数据集的散点图(代码:ScatterBubble/scatter-3.html

使用散点图揭示相关性

散点图非常适合展示数据之间的相关性。以下示例将结合 NASA/GISS 全球温度数据(Data/monthly_json.json)和夏威夷莫纳罗亚山测量的 CO2 排放数据(Data/co2_mm_mlo.csv),以探究两者之间是否存在相关性。由于莫纳罗亚山的数据仅从 1959 年后可用,我们将仅使用该年份之后的 GISS 数据。

由于我们必须加载多个文件,我们将使用 JavaScript 承诺。每个数据源被解析,并将数据发送到combine()函数,该函数返回一个可以由 Chart.js 使用的数组,如下所示:

const canvas = document.getElementById("my-scatter-chart");
 const files = ['../Data/monthly_json.json', '../Data/co2_mm_mlo.csv'];
 var promises = files.map(file => fetch(file).then(resp => resp.text()));
 Promise.all(promises).then(results => {
     const temperatures = JSON.parse(results[0]);
     const co2emissions = Papa.parse(results[1], {header: true}).data;
     const data = combine(temperatures, co2emissions);
     drawData(data);
 });

combine()函数遍历 CO2 数据中的每个对象,提取年份和月份,并使用它来获取相应的平均温度,然后创建一个包含每个月/年的 CO2 和温度的对象。每个对象被推入一个返回的数组中,如下所示:

function combine(tempData, co2Data) {
     const entries = [];
     co2Data.filter(n => +n.year >= 1959).forEach(measure => {
        const year = measure.year, month = measure.month;
        let temperature = 0;
        tempData.filter(n => n.Source=='GISTEMP' && +n.Date.split("-")
        [0] >= 1959)
                .forEach(temp => {
                     if(+temp.Date.split("-")[0] == year
                        && +temp.Date.split("-")[1] == month) {
                            temperature = temp.Mean;
                        }
                 });
        entries.push({ co2: measure.average, temp: temperature });
     });
     return entries;
 }

以下drawData()函数使用包含co2temp属性的数组对象来绘制散点图,通过将这些值复制到{x,y}对象中来实现:

function drawData(datasets) {
     const entries = [];
     datasets.forEach(entry => {
         const obj = { x: entry.co2, y: entry.temp };
         entries.push(obj);
     });
     const dataObj = {
         datasets: [
             {
                 data: entries,
                 pointRadius: 2,
                 pointBackgroundColor: 'rgba(255,0,0,.5)'
             }
         ]
     }
     const chartObj = {
         type: "scatter",
         data: dataObj,
         options: { legend: { display: false } }
     };
     new Chart("my-scatter-chart", chartObj);
 }

你可以在ScatterBubble/scatter-4.html中查看完整的代码。以下图表显示了结果,并揭示了增长中的 CO2 排放量和全球温度之间可能存在的关系。它还显示了我们应该过滤掉的一些数据,例如缺失的 CO2 测量值,这些值显示为-99.99

比较二氧化碳排放量(来源:马乌纳洛亚)和全球温度(来源:NASA;代码:ScatterBubble/scatter-4.html

我们可以通过向co2Data过滤器添加额外的谓词来过滤掉不良的测量值,如下所示:

co2Data.filter(n => +n.year >= 1959 && n.average > 0)

也是一个好主意为坐标轴添加标签,这样观众就能知道正在比较的数据类型。下面的配置添加了坐标轴标题以及图表的标题。完整的代码位于ScatterBubble/scatter-5.html中:

const chartObj = {
     type: "scatter",
     data: dataObj,
     options: {
         legend: { display: false},
         title: {
             display: true,
             text: 'CO2 emissions vs. Global temperatures',
             fontSize: 24
         },
         scales: {
             xAxes: [{
                 scaleLabel: {
                     labelString: 'CO2 emissions (ppm)',
                     display: true
                 }
             }],
             yAxes: [{
                 scaleLabel: {
                     labelString: 'Temperature anomaly (Celsius)',
                     display: true
                 }
             }],
         }
     }
 };
 new Chart("my-scatter-chart", chartObj);

最终的图表如下所示:

过滤掉错误测量值后的 CO2 与温度散点图(代码:ScatterBubble/scatter-5.html

大量数据的散点图

散点图非常适合揭示大型数据集中的隐藏模式。在下面的例子中,我们将使用从公共数据库(geonames.org)获得的大量文件来绘制散点图,显示基于纬度和经度的城市位置。该文件包含人口超过 15,000 的地点列表(Data/cities_15000.csv)。它包含超过 10 万个条目(因此,加载将需要几秒钟)。这是 CSV 文件的一般结构:

geonameid;asciiname;latitude;longitude;country_code;population;timezone14256;Azadshahr;34.79049;48.57011;IR;514102;Asia/Tehran
18918;Protaras;35.0125;34.05833;CY;20230;Asia/Nicosia
23814;Kahriz;34.3838;47.0553;IR;766706;Asia/Tehran
24851;Nurabad;34.0734;47.9725;IR;73528;Asia/Tehran
// + than 100 000 lines

要构建散点图,我们需要处理文件并将纬度和经度转换为点数据格式。坐标轴也必须配置为表示地球的圆柱投影(经度限制为-180180,纬度限制为-9090)。下面的代码配置了刻度,加载了文件,解析了数据,为每个坐标对创建了点对象,并绘制了图表:

fetch('../Data/cities15000.csv')
    .then(response => response.text())
    .then(csv => drawData(Papa.parse(csv, {header: true}).data));

 function drawData(datasets) {
     const locations = [];
     datasets.forEach(city => {
         const obj = {
             x: city.longitude,
             y: city.latitude,
             name: city.asciiname
         }
         locations.push(obj);
     });

     const dataObj = {
         datasets: [
             {
                 label: "Label",
                 data: locations,
                 pointRadius: .25,
                 pointBackgroundColor: 'red'
             }
         ]
     }

     const chartObj = {
         type: "scatter",
         data: dataObj,
         options: {
             animation: { duration: 0 },
             title: { display: false },
             responsive: false,
             legend: { display: false },
             scales: {
                 xAxes: [ { ticks: { min: -180, max: 180 } } ],
                 yAxes: [ { ticks: { min: -90,  max: 90  } } ]
             },
             tooltips: {
                 callbacks: {
                     title: (items,data) => locations[items[0].index].name
                 }
             }
         }
     };

     new Chart("my-scatter-chart", chartObj);
 }

您可以在ScatterBubble/scatter-6-world.html中查看完整的代码。结果显示了一个令人惊讶的隐藏模式(以及陆地和人类之间的相关性)。您可以将鼠标移到点上,它将显示位置名称和坐标(这是使用提示回调函数配置的):

显示超过 10 万个有人类居住地点位置的散点图(代码:ScatterBubble/scatter-6-world.html

气泡图

气泡图就像散点图一样,但它们可以在点的直径(或形状)中显示额外的变量。type属性应该是bubble。尽管它们与散点图共享相同的数据集属性,但大多数气泡图可以接收回调函数,这允许更高的交互性。气泡图的数据结构包含三个属性,如下所示:

{x: number, y: number, r: number}

当图表缩放时,xy属性会自动缩放。r属性是像素中的原始圆半径,不会缩放(但您可以在需要缩放时配置回调函数)。

以下代码(ScatterBubble/bubble-1.html)创建了一个包含五个条目的简单气泡图,每个气泡的颜色根据气泡的半径自动生成,使用回调函数:

const dataObj = {
     datasets: [
         {
             data: [{x:5, y:1, r:60},{x:3, y:1, r:30},{x:1, y:2, r:15},
                    {x:3, y:5, r:90},{x:2, y:4, r:20}],
             backgroundColor: function(context) {
                 const point = context.dataset.data[context.dataIndex];
                 return 'hsla('+(point.r * 4)+',100%,70%,.6)'
             }
         }
     ]
 }

 const chartObj = { type: "bubble", data: dataObj,
     options: {
         scales: {
             xAxes: [{ticks: {min: 0, max: 6}}],
             yAxes: [{ticks: {min: 0, max: 7}}]
         },
     }
 };
 new Chart("my-bubble-chart", chartObj);

结果如下所示。请注意,如果您调整图表大小,气泡大小不会改变:

一个简单的气泡图,包含一个数据集(代码:ScatterBubble/bubble-1.html)

当显示大量数据时,气泡图不如散点图信息丰富,但它们仍然可以揭示有趣的信息。以下示例将之前显示的散点图转换为气泡图,使用每个位置的 population 生成气泡的半径。

由于气泡可能会重叠,数据集被排序,使得人口较少的气泡保持在顶部。scaleR()函数创建了一个简单的比例,将 population 转换为圆的半径,如下所示:

fetch('../Data/cities15000.csv')
         .then(response => response.text())
         .then(csv => {
             const data = Papa.parse(csv, {header: true}).data;
             drawData(data.sort((a, b) =>  b.population - a.population));
          });

 function scaleR(value) {
     const r = Math.floor(value / 100000);
     return r != 0 ? r/10 : .25;
 }

drawData()函数为气泡图创建一个数据点对象,包含三个属性xy,包含经纬度,以及缩放后的 population 转换为半径,如下所示:

function drawData(datasets) {
     const coordset = [];
     datasets.forEach(city => {
         const obj = {
             x: city.longitude,
             y: city.latitude,
             r: scaleR(city.population)
         };
         coordset.push(obj);
     });

数据对象包含数据数组作为其数据,并配置了backgroundColor

将属性作为回调函数返回不同颜色的气泡,取决于半径的值,如下所示:

     const dataObj = {
         datasets: [
             {
                 label: "Label",
                 data: coordset,
                 backgroundColor: function(context) {
                     const value =     
                     context.dataset.data[context.dataIndex].r;
                     if(value > 20) return 'hsla(0,100%,50%,.4)';
                     if(value > 10) return 'hsla(30,100%,50%,.5)';
                     if(value > 5) return 'hsla(60,100%,50%,.6)';
                     if(value > 1) return 'hsla(120,100%,50%,.7)';
                     else return 'hsla(0,0%,50%,1)';
                 }
             }
         ]
     }

     new Chart("my-bubble-chart", {type: 'bubble', data: dataObj, options: {...});
 }

您可以在ScatterBubble/bubble-2.html中看到完整的代码。结果如下所示:

城市的气泡地图。气泡的半径与每个位置的 population 成比例(代码:ScatterBubble/bubble-2.html)

摘要

在本章中,我们学习了如何创建 Chart.js 支持的所有标准图表类型:柱状图、水平柱状图、折线图、面积图、饼图、环形图、极坐标面积图、雷达图、散点图和气泡图。

不同的图表更适合某些类型的数据集和目的,而不是其他图表。我们用不同的图表探索了相同的例子,并看到了每种类型如何传达数据的不同方面,揭示相关性、比例、趋势和隐藏的模式。

每个图表都通过一个简单的示例进行介绍,但我们还创建了一些使用公共 CSV 和 JSON 数据的真实世界可视化,这些数据需要缩小、合并、过滤并映射到 Chart.js 期望的数据格式。

我们还尝试了几个配置属性,包括图形元素、数据集和图表,允许高度定制。其中许多将在下一章中更详细地探讨。

参考文献

书籍和网站

数据来源

  • Mauna Loa CO2 测量数据:Chapter04/Data/co2_mm_mlo.csv. Pieter Tans 博士,NOAA/ESRL (www.esrl.noaa.gov/gmd/ccgg/trends/) 和 Ralph Keeling 博士,斯克里普斯海洋研究所 (scrippsco2.ucsd.edu/)。

  • 海洋温度:Chapter04/Data/monthly_json.json。GISTEMP 团队,2019:GISS 地表温度分析(GISTEMP)。NASA 戈达德太空研究所。数据访问时间:2019-02-01,data.giss.nasa.gov/gistemp/。Hansen, J., R. Ruedy, M. Sato, and K. Lo, 2010:全球地表温度变化,Rev. Geophys., 48, RG4004, doi:10.1029/2010RG000345。

  • 地理数据库:Chapter02/Data/cities1000.csv。GeoNames 地理数据库:www.geonames.org

  • 每国二氧化碳排放量(1960-2014):Chapter04/world_bank_co2_kt.json世界银行公开数据data.worldbank.org

  • 世界人口:Chapter04/WPP2017_UN.csv联合国 2017 年世界人口展望www.un.org

第五章:比例和网格配置

在本章中,你将学习如何配置控制你的图表在笛卡尔或辐射网格中显示的比例。除了饼图和环形图之外,所有图表都使用比例。笛卡尔图表,如线图、柱状图、散点图和气泡图,使用一对垂直轴,每个轴都由 Chart.js 自动计算的比例来定位数据点。图表中的数据,如极坐标面积图和雷达图,使用单个比例,将数据点放置在从中心出发的不同位置。你可以配置比例,改变数据点的显示方式,例如,使用对数比例而不是默认的线性比例来表示数值。你也可以选择顺序时间比例而不是分类比例。还有许多方法可以配置样式,并改变轴、网格线、刻度和标签在图表中的显示方式。

在本章中,我们将涵盖以下主题:

  • 配置比例

  • 笛卡尔轴、刻度和网格线

  • 辐射轴、刻度和网格线

  • 高级比例配置

配置比例

比例是一种变换,它放大或缩小数据域,以便它适合特定的范围。Chart.js 自动缩放数据,调整域数据值,使其适合图表预留的空间。比例由一个表示,这是一个表示域范围的定向线。放置在轴线上离散的值被称为刻度。具有垂直或辐射轴和离散刻度的坐标系形成一个网格。比例、轴、刻度和网格存在于所有图表中,即使你看不到它们。它们控制数据点在图表中的显示方式。

笛卡尔图表有两个比例,每个比例由垂直轴表示,xy,而辐射图表有一个比例,由半径和角度表示。辐射比例始终是线性的,但笛卡尔比例可以是线性的、对数的、分类的或时间的。Chart.js 还允许你创建自己的比例。

在大多数图表中,轴、网格线和刻度标签默认可见,但你可能希望删除不必要的线条、淡化颜色、虚线并隐藏未使用的图例,以最大化图表的数据-墨水比率。可以为所有标签配置字体大小和颜色,并且你可以使用回调函数有条件地隐藏数据。

笛卡尔配置选项

笛卡尔网格用于散点图、气泡图、柱状图和线图,包含两组比例,每组对应一个垂直轴。它们在分配给options.scales属性的物体中配置:

options: {
     scales: {
         xAxes: [{…}, ..., {…}], // array of x-axis objects
        yAxes: [{…}, ..., {…}]  // array of y-axis objects
    }
 }

你可以有每种类型的多轴。它们可以是堆叠的、并排放置或位于相对的两侧。每个轴都可以链接到特定的数据集。

极坐标面积图和雷达图使用辐射比例,并配置一个单独的options.scale属性:

options: {
     scale: {
         {…} // axis object containing configuration for the radial axis
     }
 }

笛卡尔图表中的所有轴配置对象和径向图表中的scale属性都包含一个display属性,它接收一个布尔值(truefalse),使其可见或不可见。以下代码片段隐藏了笛卡尔图表和径向图表中的所有轴、网格和标签:

options: { // configuration for a Cartesian chart
     scales: { xAxes: [{display: false}], yAxes: [{display: false}] }
 }
options: { // configuration for a radial chart
     scale: { display: false }
 }

结果在此处显示。您可以使用此效果创建 Sparklines(小型简约可视化)。请参阅Scales/scales-1-Cartesian-display.htmlscales-2-radial-display.html中的完整代码:

一个带有隐藏轴、网格线和刻度标签的笛卡尔图表和一个径向图表。

代码:Scales/scales-1-Cartesian-display.htmlscales-2-radial-display.html

坐标轴还支持十多个回调函数,可以用来配置每个轴显示的标签、刻度和其他数据。

笛卡尔坐标轴、刻度和网格线

有五种使用笛卡尔网格的图表类型:柱状图、水平柱状图、线图、散点图和气泡图。每个图表都有两个刻度,每个垂直轴一个。每个刻度可以是四种类型之一:

  • type:'linear': 一个可以用来比较相同量级数值的数值刻度。

  • type:'logarithmic': 一个用于比较不同量级数值的数值刻度。

  • type:'category': 一个无序的分类列表。

  • type:'time': 一个有序的瞬间列表。这个刻度需要moment.js库。

在大多数图表中,至少有一个刻度是数值的(线性或对数)。在散点图和气泡图中,两个刻度都是数值的。时间序列图使用数值刻度和时间刻度,但也可以使用分类刻度。您还可以创建两个刻度都是分类的相关图表。

以下表格列出了所有笛卡尔轴的常见配置选项(最后三个是对象,它们包含将在单独部分中描述的特定配置参数):

属性 描述
type 'logarithmic', 'linear' (散点图和气泡图中的两个轴的默认值,以及线图和柱状图的 y 轴的默认值), 'category' (柱状图和线图的 x 轴的默认值),  'time' (需要moment.js库) 选择轴类型。注意,某些配置属性是特定于某些类型的轴的,而其他可能不被支持。
position topbottom (xAxes 的默认值) for xAxes; left (默认) 或 right for yAxes 轴的位置。如果有多个轴位于同一位置,它将被放置在现有轴的下方或左侧。您可以使用weight对它们进行排序。请参阅Cartesian/Cartesian-1-position.html
weight Number 当有多个轴位于同一位置时,按顺序排列轴。较大的数字将轴放置在图表之外更远的位置。请参阅Cartesian/Cartesian-2-weight.html
offset truefalse(默认值适用于所有轴,除了条形图中的 type:'category' 如果为 true,则在轴的每一边添加空间。
id String 标记轴以便在多个轴使用时与数据集相关联。
gridLines Object 配置网格线。
scaleLabel Object 配置刻度标题。
ticks Object 配置刻度。

Cartesian 规模的配置属性和对象

此外,还有 14 个未在此列出的生命周期回调函数。这些属性始终用于 scales.xAxesscales.yAxes 数组中的对象内。以下是一个典型的配置示例:

options: {
     scales: {
         xAxes: [{
             ticks: {…},
             scaleLabel: {…},
             gridLines: {…}
         }],
         yAxes: [{
             type: 'logarithmic', 
             position: 'left',
             scaleLabel: {…},
         ]
     }
 }

这种多级嵌套层次结构有时可能会让人困惑。一个常见的错误是将属性放置在错误的对象中;不会显示错误消息,但什么也不会发生。在本章中,我们将使用 axis 来指代 xAxesyAxes 中的任何轴对象(例如 axis.ticks.minoraxis.scaleLabel),或者使用从 scales 对象的完整路径(例如 scale.yAxes[0].ticks.minor)。例外情况是当它们在表中列出时,但在此情况下,父对象将会有限定。

数值 Cartesian 规模

有两种数值规模。在所有使用数值规模的图表中,type:'linear' 是默认值,但并不总是最佳选择。线性图表最适合比较相同量级的数据点,但当样本中包含一些值是其他值的数百倍时,数据相关性可能难以发现。

线性规模

在以下散点图中使用了线性规模,该图绘制了几个国家的总人口,比较了它们在 1980 年(y 轴)和 2015 年(x 轴)的人口。数据来自联合国(有关此章节的 GitHub 存储库中的 Data/WPP2017_UNH.csv)。中位数线表示人口相同的点。位于中间线以上阴影区域的国家经历了人口减少:

显示 1980 年至 2015 年人口增减的图表。由于中国、印度和世界其他地区量级的不同,线性规模不是最佳选择

代码:Numeric/numeric-1-linear.html

图表右侧的两个点代表印度和中国。所有其他国家都集中在图表的左下角。由于它混合了不同量级的值,这个图表非常难以阅读。大多数国家人口数量较小(以百万计),当与中国和印度(以十亿计)相比时,它们最终会杂乱无章地出现在刻度的开始部分。在这些情况下,我们应该使用对数规模。

工具提示显示图表中每个国家的名称。您可以在 Numeric/numeric-1-linear.html 中查看此图表的完整代码。这是一个混合图表,包含不同类型的数据集(线性和散点)。我们将在 第七章,高级 Chart.js 中探讨混合图表。

对数尺度

将轴对象的 type 属性声明为对数将根据对数尺度渲染其数据。将上一个示例中的一个轴更改为 type: 'logarithmic',则线条将变成曲线,如下所示。在这个可视化中,数据点的分布似乎有所改善。它们更接近,图表揭示了之前不可见的一些数据:

将其中一个尺度设置为对数尺度可以改进散点图,揭示隐藏的数据

代码:Numeric/numeric-2-log.html

我们可以改进它。由于两个尺度都包含相同的人口数据,我们可以将两个轴的类型都声明为对数,如下所示。现在曲线再次变成直线,点更接近,更容易比较,并且揭示了更多隐藏的数据:

具有两个对数轴的更好的散点图

代码:Numeric/numeric-3-log.html

最后两个示例的完整代码在 Numeric/numeric-2-log.htmlnumeric-3-log.html

配置轴标题

您可以使用 axis.scaleLabel 属性为任何 Cartesian 图表中的每个轴添加标签或标题(例如,options.scales.xAxes[0].scaleLabel 配置第一个 x 轴的标题)。以下表格列出了 scaleLabel 对象的可配置属性:

属性 描述
display truefalse (默认) 显示或隐藏轴标题
labelString String (默认为 '') 轴的标题
lineHeight Number 文本上方和下方的间距
fontColor, fontFamily, fontStyle String CSS 字体属性
fontSize Number 像素字体大小
padding Number 文本前后间距

Cartesian 尺度的刻度标签(刻度标题)配置。这些属性用于任何 axis.scaleLabel 对象。

以下代码片段为我们在 第三章,Chart.js – 快速入门 中创建的柱状图的 x 轴和 y 轴添加标题。请注意,仅添加 axis.scaleLabel.labelString 是不够的,您还必须设置 axis.scaleLabel.display: true,因为标题默认是隐藏的:

scales: {
     xAxes: [{
         scaleLabel: {
             display: true,
             labelString: "Oceans",
             fontSize: 16
         }
     }],
     yAxes: [{
         scaleLabel: {
             display: true,
             labelString: "Volume in cubic km",
             fontSize: 16
         }
     }]
 }

Cartesian/Cartesian-3-scaleLabel.html 中查看完整代码。结果如下所示:

使用 *scaleLabel* 属性添加轴标题。代码:Cartesian/Cartesian-3-scaleLabel.html.

配置刻度

刻度是沿着轴放置的离散点。它们的位置决定了数据点相对于轴的绘制方式。在数值尺度中,axis.ticks 属性配置轴将显示的最大和最小数值参数以及要显示的刻度数量。在任何笛卡尔尺度中,它可以用来应用刻度标签的样式并配置填充和其他定位参数。刻度标记在 axis.gridLines 属性中单独配置。

以下表格列出了可以为任何笛卡尔尺度配置的刻度属性:

属性 描述
display true(默认)或 false 显示或隐藏刻度标签。
fontSize Number 像素字体大小。
fontColor, fontFamily, fontStyle String CSS 字体属性。
reverse truefalse(默认) 反转刻度标签的顺序。
callback 函数。默认: d=>d 函数接收刻度的值。它可以用来隐藏刻度或更改显示的值。
labelOffset Number. 默认: 0 从刻度的中心点偏移标签。
mirror truefalse(默认) 将标签翻转围绕轴到图表内部。
padding Number. 默认: 10 刻度标签与轴之间的空间。
autoSkip true(默认)或 false 如果水平标签空间不足,则跳过标签。autoSkip:true 总是显示它们。
maxRotation Number. 默认: 90 xAxis 中标签的最大旋转角度。
minRotation Number. 默认: 0 xAxis 中标签的最小旋转角度。

笛卡尔尺度的刻度配置。这些属性用于任何 axis.ticks 对象。

以下表格列出了由数值尺度(线性或对数)支持的附加刻度属性:

属性 描述
min Number 轴的下限。
max Number 轴的上限。
suggestedMin Number 如果数据的最大值更大,则将其设置为最小值。
suggestedMax Number 如果数据的最大值更小,则将其设置为最大值。
beginAtZero true(默认)或 false 强制轴使用零作为下限。
stepSize Number 设置刻度之间最小步长。覆盖精度。
maxTicksLimit Number. 默认是 11 明确设置轴的最大刻度数。

线性和对数尺度的刻度配置。这些属性用于这些尺度的任何 axis.ticks 对象

以下配置应用于我们在 第三章,Chart.js – 快速入门 中创建的一个条形图,它使用 axis.ticks.callback 属性将单词 ocean 作为后缀添加到水平轴的刻度标签中。垂直轴被反转,使得条形看起来是倒置的:

scales: {
     xAxes: [{
         ticks: {
             callback: d => d + ' ocean'
         }
     }],
     yAxes: [{
         ticks: {
             reverse: true,
         }
     }]
 }

结果在此显示。完整代码请见 Cartesian/Cartesian-4-ticks-style.html:

图片

在 Cartesian 图表中配置刻度,反转垂直轴,并在水平轴标签中添加文本使用 axis.ticks.callback

代码:Cartesian/Cartesian-4-ticks-style.html

Chart.js 会自动计算每个轴的最小范围,以便以最有效的方式渲染数据。但您可以使用 axis.ticks.minaxis.ticks.max 属性显式设置最小和最大值。在这种情况下,任何超出范围的图表部分将不会显示。或者,您可以使用 axis.ticks.suggestedMinaxis.ticks.suggestedMax,这也会限制范围,但前提是没有数据值被省略。以下代码将这些属性应用于散点图,并通过将 axis.ticks.stepSize 设置为较小的值来添加更多刻度(默认最大值为 11):

scales: {
     xAxes: [{
         ticks: {
             padding: 10,
             stepSize: 20,
         }
     }],
     yAxes: [{
         ticks: {
             padding: 10,
             min: -0.6,
             suggestedMax: 0.6, // ignored, because data is larger

         }
     }]
 }

此配置的结果如下所示。完整代码位于 Cartesian/Cartesian-5-ticks-minmax.html:

图片

数值图表中的刻度配置:步长和最小值

代码:Cartesian/Cartesian-5-ticks-minmax.html

配置网格线

Cartesian 网格支持多个属性,这些属性会改变屏幕上网格线的显示。您可以更改颜色、线宽、线型、刻度大小、网格线的间距,以及零线的不同样式。您还可以显示或隐藏网格线、刻度和边框,减少不必要的图表元素,使图表更高效。

这些属性配置在每个 xAxesyAxes 数组对象中的 gridLines 对象内,并在此列出:

属性 描述
display true (默认) 或 false 显示或隐藏此轴的网格线。
color CSS 颜色或颜色数组;默认为 ‘rgba(0,0,0,.1)’ 网格线的颜色。如果使用数组,则为每条线设置颜色。
lineWidth Number; 默认为 1 网格线的宽度。
borderDash Number[] 网格线的虚线数组。
borderDashOffset Number 网格线的虚线偏移量。
drawBorder true (默认) 或 false 绘制/隐藏坐标轴线。
drawOnChartArea true (默认) 或 false 在图表区域内绘制/隐藏坐标轴的网格线。
drawTicks true (默认) 或 false 绘制/隐藏刻度标记。
tickMarkLength Number 刻度标记的大小。
zeroLineWidth Number 零线的宽度。
zeroLineColor CSS 颜色 零线的颜色。
zeroLineBorderDash Number[] 零线的虚线数组。
zeroLineBorderDashOffset Number 零线的虚线偏移量。
offsetGridLines truefalse 将网格线移动到标签之间(默认在柱状图中)。

Cartesian 刻度中网格线的配置。这些属性用于任何axis.gridLines对象

这里展示了几个网格线配置示例。此代码为垂直网格线应用了不同的颜色,并为水平网格线应用了虚线数组。由于axis.gridLines.drawBorder设置为false,轴线被隐藏。两个轴的零线都应用了不同的宽度和颜色:

scales: {
     xAxes: [{
         gridLines: {
             color: ['#fff','#d30','#b33',...,'#09b','#09e'],
             lineWidth: 2,
             zeroLineColor: 'black',
             zeroLineWidth: 5,
             drawBorder: false
         },
         ticks: {
             padding: 10,
             callback: function(d) {return d != 200 ? d : undefined;}
         }
     }],
     yAxes: [{
         gridLines: {
             zeroLineColor: 'black',
             zeroLineWidth: 5,
             lineWidth: 2,
             borderDash: [5, 5],
             drawBorder: false
         },
         ticks: { padding: 10 }
     }]
 }

结果显示在下方的屏幕截图上。完整代码位于Cartesian/Cartesian-6-grid-styles.html

垂直网格线具有不同的颜色,水平网格线具有虚线数组。两个轴线都被隐藏,使用axis.gridLines.drawBorder: false

代码:Cartesian/Cartesian-6-grid-styles.html

刻度线是穿过图表区域外的线条。您可以使用axis.gridLines.drawTicks:false来隐藏它们,或者使用axis.gridLines.tickMarkLength来调整它们的长度。您可以使用axis.gridLines.drawOnChartArea:false在图表区域内隐藏gridLines,以及使用axis.gridLines.drawBorder:false隐藏轴线。这些属性被用于配置以下图表(Cartesian/Cartesian-7-grid-styles.html):

垂直网格具有 15 像素的axis.gridLines.tickMarkLengthaxis.gridLines.drawOnChartArea: false。水平网格隐藏轴,使用axis.gridLines.drawBorder: false

代码:Cartesian/Cartesian-7-grid-styles.html

此配置隐藏了ticksgridLines以生成具有单个居中y轴的简约图表:

options: {
     scales: {
         xAxes: [{
                ticks: { display: false },
             gridLines: { display: false }
         }],
         yAxes: [{
                ticks: {
                 mirror: true,
                 padding: -(canvas.width/2)
             },
             gridLines: {
                 drawBorder: false,
                 drawOnChartArea: false,
                 drawTicks: false,
                 offsetGridLines: true
             }
         }]
     }
 }

将结果应用于折线图显示如下。完整代码请见Cartesian/Cartesian-8-grid-minimal.html

具有最小网格标记的图表 代码:Cartesian/Cartesian-8-grid-minimal.html

类别刻度

典型的柱状图和折线图使用类别刻度作为x轴,数值刻度作为y轴。多个数据集重用相同的类别数据。在这些图表中,用于类别轴的值来自数据对象的labels属性。如果数据集有labels属性,其x轴将自动定义为type:category

配置轴

类别刻度与数值图表具有相同的轴配置,但在axisaxis.ticks对象中支持一些额外的属性。axis对象有一个额外的属性,可以用来覆盖轴的数据对象labels

属性 描述
labels 字符串数组 要显示的标签数组。覆盖任何其他标签定义,包括数据对象属性:labelsxLabelsyLabels

类别刻度轴的附加配置

以下代码片段显示了在三个不同属性中定义的类别标签。由于单个x轴包含labels属性,它将覆盖所有之前的定义:

new Chart("my-chart",
    type: ...,
    data: {
        labels: ['One', 'Two', 'Three'], // used if others are not present
        xLabels: ['ONE', 'TWO', 'THREE'],   // overrides ‘labels’     
        datasets: […]
    },
    options: {
        scales: {
            xAxes: [{
                type: 'category',
                labels: ['Label 1', 'Label 2', 'Label 3'] // overrides 
                xLabels
            }]
        }
    }
});

你可以使用 Chart.js 中的 xLabelsyLabels 属性在数据对象中创建具有类别尺度的图表,适用于 xy 轴。每种类型的第一个轴将使用它们。dataxLabels 数组具有相同的大小。xLabels 数组中的每个元素都与 data 数组中的相应项相关联,该数组包含来自 yLabels 数组的值。这创建了一个类别之间的多对一关系。许多 x 值共享一个单一的 y 值。

在以下示例中,使用 Global.defaults.scale 为所有轴设置了属性:

const yLabels = ["Water", "Land", "Air"]; // groups: multiple points
const xLabels = ["Ship", "Train", "Bike", "Cruiser",
                 "Jet", "Bus", "Rocket", "Car"]; // items: single point
const data = ["Water", "Land", "Land", "Water", "Air", "Land", "Air",           
              "Land"];

const dataObj = {
    xLabels: xLabels, // used by x-axis category scale
    yLabels: yLabels, // used by y-axis category scale
    datasets: [
        {
            data: data,
            pointRadius: 50, pointHoverRadius: 50,
            pointStyle: 'rectRot',
            showLine: false,
            backgroundColor: "hsla(20,100%,80%,0.8)",
            borderColor: "hsla(0,100%,50%,1)"
        }
    ]
}

 Chart.defaults.scale.gridLines.drawBorder = false;
 Chart.defaults.scale.gridLines.lineWidth = 10;
 Chart.defaults.scale.gridLines.drawBorder = false;
 Chart.defaults.scale.offset = true;
 Chart.defaults.scale.ticks.padding = 20;

 new Chart("correlation",
         {
             type: "line",
             data: dataObj,
             options: {
                 legend: {display: false},
                 scales: {
                     xAxes: [{type: 'category'}],
                     yAxes: [{type: 'category'}]
                 },
                 animation: {duration: 0},
                 tooltips: {displayColors: false}
             }
         });

结果如下所示。请参阅 Category/category-1-one-to-many.html 中的完整代码:

图片

使用两个 type:’category’ 轴创建的许多到一关系的关联图。

代码:Category/category-1-one-to-many.html.

你也可以创建许多到多类别的关联关系,但它不会与类别尺度一起工作。你必须设置一个带有两个数值线性尺度的散点图,然后使用回调将数字映射回类别。以下代码显示了如何做到这一点:

const xLabels = ["Lake","River","Road","Railroad","Ocean","Air"];
 const yLabels = ["Car","Bus","Airplane","Sailboat","Cruiser","Train",
                  "Bike"]
 const data = [
     {x: 1, y: 4}, {x: 1, y: 5}, {x: 2, y: 4}, {x: 3, y: 1}, {x: 3, y: 2},
     {x: 3, y: 7}, {x: 4, y: 6}, {x: 5, y: 5}, {x: 6, y: 3}
 ];

 const dataObj = {
     datasets: [
         {
             data: data,
             pointRadius: 20, pointHoverRadius: 20,
             pointStyle: 'rectRot',
             backgroundColor: "hsla(20,100%,80%,0.8)",
             borderColor: "hsla(0,100%,50%,1)"
         }
     ]
 }

 Chart.defaults.scale.gridLines.drawBorder = false;
 Chart.defaults.scale.gridLines.lineWidth = 2;
 Chart.defaults.scale.gridLines.color = 'red';
 Chart.defaults.scale.offset = true;
 Chart.defaults.scale.ticks.padding = 10;
 Chart.defaults.scale.ticks.min = 0;

 new Chart("correlation",
         {
             type: "scatter",
             data: dataObj,
             options: {
                 legend: {display: false},
                 animation: { duration: 0 },
                 scales: {
                     xAxes: [{
                         ticks: {
                             max: 7,
                             callback: function(value) {
                                 return xLabels[value-1];
                             }
                         }
                     }],
                     yAxes: [{
                         ticks: {
                             max: 8,
                             callback: function(value) {
                                 return yLabels[value-1];
                             }
                         }
                     }]
                 }
             }
         });

结果如下所示。请参阅 Category/category-2-many-to-many.html 中的完整代码:

图片

使用两个 type:'linear' 轴和将数值映射到类别的许多到多关系的关联图

代码:Category/category-2-many-to-many.html

配置刻度

类别尺度扩展了笛卡尔图表的 axis.ticks 配置,并增加了三个额外的属性,如下表所示:

属性 描述
labels String 标签 labels 数组。这将覆盖之前对此数组的任何声明。
min String axis.ticks.labels 中的一个字符串,表示类别数据的下限。
max String axis.ticks.labels 中的一个字符串,表示类别数据的上限。

类别尺度的额外刻度配置属性

axis.ticks.minaxis.ticks.max 属性取决于字符串在 axis.ticks.labels 数组中的出现顺序。如果顺序相反,结果可能是一个空图表。

这是我们在 第三章,Chart.js – 快速入门 中创建的条形图,类别标签顺序相反,并且有五个条形而不是七个。缺失的条形是通过类别尺度的 axis.ticks.minaxis.ticks.max 属性移除的,这些属性接受字符串:

scales: {
     xAxes: [{ // category axis
         display: true,
         ticks: {
             labels: labels.reverse(), // overrides labels array
             min: 'South Pacific',
             max: 'North Atlantic'
         }
     }]
 }

结果如下所示。请参阅 Category/category-3-minmax.html 中的完整代码:

图片

使用 axis.ticks.minaxis.ticks.max 来限制类别尺度的范围。

代码:Category/category-3-minmax.html.

配置网格线

类别尺度与数值尺度使用的相同 axis.gridLines 配置。

时间尺度

你可以使用简单的字符串类别来表示日期和时间信息,但通过使用时间类型的轴,你可以解析、格式化和生成时间数据。这提供了更大的灵活性和交互性。

时间尺度需要 moment.js 库(momentjs.com)。要使用时间尺度,你可以导入 moment.js 库或将 Chart.bundle.js 库包含在你的页面中。最好导入 moment.js,因为你可能还想使用其他日期和时间函数。你可以通过将其包含在页面中通过 CDN 来实现:

<script src="img/moment.js">
 </script>

数据通常使用点结构配置,其中 x 属性是日期,y 属性是某个定量值。你也可以使用 t 属性代替 x。许多标准日期格式可以自动解析。以下是一些有效的时间数据点:

{x: new Date(), y: 1} // now
{t: ‘20190224’, y: 2} // 2019-02-24

你也可以在简单的数据对象 labels 数组中包含日期:

new Chart("my-chart", {
     type: "bar",
     data: {
         labels: [‘20190224’, ‘20190227’, ‘20190305’],
         datasets: [...],
     }
 });

这里有一个最小示例。此代码使用 moment.js 库生成一个日期列表,并创建一个包含 10 个日期的数据集。它使用所有时间尺度属性的默认值,除了 axis.time.unit,它通知应使用的单位:

const dataset = [];
 let date = moment('20181120');
 for(let i = 1; i <= 10; i+= 1) {
     dataset.push({t: date, y: Math.random() * 10});
     date = moment(date)
            .add( Math.floor(Math.random() * 10)+1, 'days').calendar();
 }
 const dataObj = {
     datasets: [{data: dataset, backgroundColor: 'hsla(290,100%,45%,.5)'}]
 }
 new Chart("my-chart", {
     type: "bar",
     data: dataObj,
     options: {
         legend: {display: false},
         scales: {
             xAxes: [{
                 type: 'time',
                 offset: true,
                 gridLines: { offsetGridLines: true },
                 time: {unit: 'day'}
             }]
         }
     }
 });

偏移量移动条形和 gridLines,使它们保持在图表内。这是具有类别尺度的条形图的默认设置,但不是时间尺度的。结果如下所示(Time/time-1.html)。请注意,条形图不是等间距的,但时间间隔是。这是默认配置,但你可以更改它,就像我们接下来要看到的那样:

使用默认配置的时间尺度的条形图。代码:Time/time-1-html.

配置时间格式

特定于时间尺度的属性配置在 axis.time 属性中。它接收一个对象。以下列出了其中的一些属性:

属性 描述
unit millisecond, second, minute, hour, day, week, month, quarter, year 考虑数据的时间单位。
stepSize Number 尺度中值之间的最小步长(这可能在单个刻度中组合多个值)。
displayFormats 对象,包含零个或多个时间单位作为字符串属性 此对象用于覆盖每个单位使用的默认字符串格式。以下列出了支持的标记。
tooltipFormat 包含日期格式的字符串,例如 MMMMYYYYh:mm:ss 用于在工具提示中显示数据/时间信息的格式字符串。以下列出了支持的标记。

axis.time 对象的选定属性

时间尺度使用 moment.js 格式,这些格式基于标准的日期/时间格式标记。以下列出了每个单位的默认格式及其产生的输出:

单位 默认格式 输出示例
Millisecond h:mm:ss.SSS A 2:07:36.976 PM
Second h:mm:ss A 2:07:36 PM
分钟 h:mm A 2:07 PM
Hour hA 2 PM
Day MMM D Feb 24
Week ll Feb 24 2019
Month MMM YYYY Feb 2019
Quarter [Q]Q - YYYY Q1 – 2019
Year YYYY 2019

时间刻度中使用的默认时间单位格式

如果您希望以特定方式格式化日期/时间,可以使用 axis.time.displayFormats 属性来覆盖您所使用的时间单位默认格式:

time: {
     unit: 'month',
     displayFormats: {
         month: 'MMMM',   // will print January, February,… for month units
     }
 }

您可以将以下标记组合起来创建一个字符串:

Property Tokens Output
Day of month D, Do, DD 1 2 … 31, 1st 2nd … 31st, 01 02 … 31
Day of week d, ddd, dddd 0 1 … 6, Sun Mon … Sat, Sunday, Monday … Saturday
Month M, MM, MMM, MMMM 1 2 … 12, 01 02 … 12, Jan Feb … Dec, January February … December
Quarter Q, Qo 1 2 3 4, 1st 2nd 3rd 4th
Year YYYY, Y 1970 1971 … 2030, 1970 1971 … 9999
AM/PM A, a AM PM, am pm
AM/PM A, a AM PM, am pm
Hour H, HH, h, hh 0 1 … 23, 00 01 … 23, 1 2 … 12, 01 02 … 12
Minute m, mm 0 1 … 59, 00 01 … 59
Second s, ss 0 1 … 59, 00 01 … 59
Millisecond SSS 000 001 … 999
Time zone ZZ -0700 -0600 … +0700

创建日期字符串格式的最常见标记

此外,还有其他本地化格式。请参阅 moment.jsmomentjs.com/docs/#/displaying/format)文档中的附加选项。

配置轴

时间刻度支持所有笛卡尔刻度的属性,并增加两个额外的属性,如下表所示。这些属性在每个轴中配置(例如,scales.xAxes[0].bounds):

Property Value Description
bounds data (default), ticks 设置刻度边界策略。默认数据将调整轴的维度以适应数据。使用刻度,图表将被截断以适应刻度。
distribution linear (default) or series 数据在轴上的分布方式。如果是系列,数据值将均匀分布。如果是线性,瞬间将均匀分布。

时间刻度的附加轴配置属性

在最后一个例子中,条形位置不均匀,因为默认分布保留了时间瞬间。在这个例子中,条形将均匀分布,但它们之间的周期将不均匀。数据由刻度限制,而不是数据点,日期格式显示缩写月份、日和年:

xAxes: [{
     type: 'time',
     offset: true,
     gridLines: { offsetGridLines: true },
     distribution: 'series',
     bounds: 'ticks',
     time: {
         unit: 'day',
         displayFormats: {
             day: 'MMM D Y',
         }
     }
 }]

结果如下所示。请参阅 Time/time-2.html 中的完整代码:

一个具有等距条形和刻度界限的时间刻度图表。代码:Time/time-2.html.

配置刻度

时间刻度扩展了笛卡尔图表的 axis.ticks 配置,并增加了一个额外的属性,如下所示:

Property Value Description
source auto(默认),data(默认,如果数据在点格式中),labels(默认,如果数据在数组格式中) 选择从何处获取时间刻度的条目。

时间刻度的时间刻度配置属性

axis.ticks.source 属性允许您选择时间刻度的数据源。如果您的数据集是一个简单的数组,并且日期位于标签数组中,则默认配置将自动从那里获取日期。您也可以显式设置此属性:

const dataset = [], labels = [];
 let date = moment('20181120');
 for(let i = 1; i <= 10; i+= 1) {
     labels.push(date);
     dataset.push(Math.random() * 10);
     date = moment(date)
            .add( Math.floor(Math.random() * 10)+1, 'days').calendar();
 }

 const dataObj = {
     labels: labels,
     datasets: [{
         data: dataset,
         backgroundColor: 'hsla(290,100%,45%,.5)'
     }]
 }

 new Chart("my-chart", { type: "bar", data: dataObj,
     options: {
         scales: {
             xAxes: [{
                 // ... other configuration not shown
                 ticks: { source: 'labels' }
             }]
         }
     }
 });

结果在此处显示。请参阅完整的代码 Time/time-3.html

使用 labels 数组作为刻度数据源的时间刻度的柱状图。代码:Time/time-3.html

配置网格线

时间刻度与数值刻度使用的相同 axis.gridLines 配置。

径向刻度

两个 Chart.js 图表类型使用径向刻度:radarpolarArea。具有单个刻度的径向图表使用此处列出的属性进行配置。一些属性与笛卡尔刻度中使用的属性类似,但它们的配置选项较少:

属性 描述
pointLabels Object 配置点标签
ticks Object 配置图表刻度
angleLines Object 配置径向网格线
gridLines Object 配置同心网格线

径向刻度的配置对象

这些属性直接用于 options.scale 对象内部。例如:

options: {
     scale: {
         ticks: {…},
         angleLines: {…},
         gridLines: {…},
         pointLabels: {…}
     }
 }

配置点标签

点标签是围绕径向图表显示的标签,位于每个角度线上。刻度标签放置在图表内部,覆盖第一条角度线,并带有背景。点标签通过 scale.pointLabels 属性进行配置,该属性是一个具有以下属性的对象:

属性 描述
display true (默认在雷达图中) 或 false (默认在极区图中) 显示或隐藏轴标签
callback 函数;默认为 d=>d 为每个点返回值标签
fontColorfontFamilyfontStyle 包含 CSS 字体属性的字符串 字体属性
fontSize 数字 像素字体大小

径向轴点标签配置属性

您可以使用 display:false 在径向图表中隐藏 pointLabels(请参阅以下图例)。如果您使用极区图表,并且想要使它们可见,可以使用 display:true。请参阅 Radial/radial-1-pointLabels-hide.htmlradial-2-pointLabels-polar.html

以下代码使用一些其他的 pointLabel 属性来改变雷达图中标签的颜色,并使用回调函数向每个标签添加文本:

scale: {
     pointLabels: { callback: (d) => 'Step ' + d, fontColor: 'red'}
 }

请参阅完整的代码 Radial/radial-3-pointLabels-callback.html。结果如下所示:

径向刻度标签配置。左侧:使用 scale.pointLabels.display *= false* 隐藏标签。右侧:使用回调函数更改颜色并附加文本。代码:Radial/radial-1-pointLabels-hide.htmlradial-3-pointLabels-callback.html

配置刻度

径向轴的刻度是同心圆(可以渲染为圆形或多边形,每个角度线都有一个顶点)。刻度标签放置在圆圈上,并在其后面有背景。

刻度在 scale.ticks 对象中配置,具有以下属性:

属性 描述
display true(默认)或 false 显示或隐藏刻度标签。
fontSize Number 像素字体大小。
fontColor, fontFamily, fontStyle String CSS 字体属性。
reverse truefalse(默认) 反转刻度标签的顺序。
callback 函数;默认:d=>d 函数接收刻度的值。它可以用来隐藏刻度或更改显示的值。
min Number 轴的下限。
max Number 轴的上限。
suggestedMin Number 如果数据的最大值更大,则将此设置为最小值。
suggestedMax Number 如果数据的最大值更小,则将此设置为最大值。
beginAtZero true(默认)或 false 强制轴使用零作为下限。
stepSize Number 设置刻度之间最小步长。
maxTicksLimit Number; 默认是 11 明确设置轴的最大刻度数。
showLabelBackdrop truefalse; 默认是 true 在网格线上绘制刻度标签的背景。
backdropPaddingX backdropPaddingY Number; 默认是 2 背景填充。
backdropColor Color; 默认是 rgba(255,255,255,.75) 标签背景颜色。

径向刻度的属性

刻度属性类似于用于笛卡尔线性图表的属性。以下代码显示了应用于雷达图的一些刻度配置:

scale: {
     ticks: {
         fontColor: 'blue',
         callback: (d) => d + ' m',
         reverse: true,
         min: 0,
     }
 }

结果在此处显示。请参阅完整的代码 Radial/radial-4-ticks.html

具有某些刻度配置的雷达图。代码:Radial/radial-4-ticks.html

配置网格和角度线

以下属性用于配置径向网格的径向线(scale.angleLines)和同心圆或多边形(scale.gridLines):

属性 描述
display true(雷达图默认)或 false(极坐标区域默认) 显示或隐藏线条。
color Color 线的颜色。
circular true(极坐标区域默认)或 false(雷达图默认) 仅在 gridLines 对象中。如果为 true,则 gridLines 是圆形的。否则,它们是点之间的直线。
lineWidth Number 线条的宽度。

径向网格中网格和角度线的配置

以下代码在雷达图中配置了多个网格和角度线的属性。它将网格线改为圆形,并更改了线宽和颜色:

scale: {
     gridLines: {
         circular: true,
         lineWidth: 2,
         color: 'hsla(240,100%,50%,.2)'
     },
     angleLines: {
         display: true,
         lineWidth: 2,
         color: 'hsla(120,100%,25%,.2)'
     }
 }

将结果应用于雷达图,如下所示。完整代码请见 Radial/radial-5-grid-angle-lines.html

配置了网格和角度线的雷达图。代码:Radial/radial-5-grid-angle-lines.html

高级刻度配置

本节简要概述了一些您可能不太经常使用的配置。有关这些主题的更多详细信息,请参阅官方文档。

多个笛卡尔坐标轴

在二维笛卡尔网格中绘制数据只需要两个坐标轴,但如果需要,您可以添加更多。您可能希望在一个图表的两侧重复坐标轴标题或刻度标签以提高清晰度。您还可以显示具有不同刻度的两个数据集(尽管这在数据可视化中通常是不良做法)。

如果您有多个坐标轴,可以使用 axis.weightaxis.position 属性来控制它们的相对位置。除非您使用 id 属性将坐标轴连接到特定的数据集,否则 yAxis 数组中的第一个坐标轴将用于所有数据集。数据集通过 yAxisIDxAxisID 属性链接到坐标轴,这些属性引用坐标轴的 ID。有关示例,请参阅 Advanced/adv-1-position-evil.html

以下代码片段配置了一个图表的三个坐标轴,并将它们放置在图表的不同侧面。它没有明确链接任何数据集,因为它们都使用相同的刻度:

scales: {
     yAxes: [{
         id: 'y-axis-1',
         ticks: {min: -2,max: 2},
         scaleLabel: {display: true, labelString: "Left Axis"},
         position: 'left'
     },{
         id: 'y-axis-2',
         ticks: {min: -2, max: 2},
         scaleLabel: {display: true,labelString: "Right Axis"},
         gridLines: {display: false},
         position: 'right'
     }],
     xAxes: [{
         ticks: {min: -4, max: 4},
         scaleLabel: {display: true,labelString: "Top Axis"},
         position: 'top'
     }],
 }

完整代码请见 Cartesian/Cartesian-1-position.html。结果如下所示:

在不同位置的三个坐标轴的图表。代码:Cartesian/Cartesian-1-position.html.

您也可以将坐标轴堆叠在同一侧,如下所示。这在类别刻度中添加上下文时很有用。在这个例子中,为海洋添加了一个额外的类别刻度:

const labels = ["Arctic", "North Atlantic", "South Atlantic", "Indian",
                 "North Pacific", "South Pacific", "Southern"];
 const labels2 = ["","Atlantic", "", "Pacific",""];
// ...
xAxes: [
     {
         weight: 10,
         labels: labels,
         ticks: {
             fontColor: 'black'
         }
     },{
         weight: 20,
         labels: labels2,
         ticks: {
             fontColor: 'purple'
         },
         offset: true
     },
 ]

结果如下所示。完整代码请见 Cartesian/Cartesian-2-weight.html

在同一侧有两个类别坐标轴的图表。代码:Cartesian/Cartesian-2-weight.html.

可能最好隐藏或配置第二个类别刻度的网格线,以免它们泄漏到图表区域。

回调函数

如果您需要过滤或更改单个刻度标签,可以使用生命周期回调函数。共有 14 个这样的回调函数,它们直接在每个坐标轴对象中进行配置。以下代码示例展示了如何根据刻度值(增加步长)进行过滤,并在稍后的阶段更改要显示的值(在这个阶段,更改不再影响图表):

yAxes: [
     {
         afterBuildTicks: function(axis) {
             log('afterBuildTicks')
             axis.ticks = axis.ticks.filter((d,i) => d % 100000 == 0);

         },
         afterTickToLabelConversion: function(axis) {
             log('afterTickToLabelConversion')
             axis.ticks.forEach((d,i) => axis.ticks[i] = +d/1000);
         }
     }
 ]

完整代码请见 Advanced/adv-2-callbacks.htmladv-3-radial-callbacks.html

规模服务

可以使用刻度服务在变化时更新刻度。你可以使用它来传递一个部分配置,该配置将与当前配置合并。在以下代码中,它被用来设置线性刻度中刻度的最小边界,并在类别刻度的刻度标签中添加文本:

Chart.scaleService.updateScaleDefaults('linear', {
     ticks: {
         min: -100000
     },
     afterTickToLabelConversion: function(axis) {
         axis.ticks.forEach((d,i) => axis.ticks[i] = +d/1000);
     }
 });

 Chart.scaleService.updateScaleDefaults('category', {
     afterTickToLabelConversion: function(axis) {
         axis.ticks.forEach((d,i) => axis.ticks[i] = d + " Ocean")
     }
 });

Advanced/adv-4-scaleService.html 中查看完整代码。

摘要

在本章中,你学习了如何配置 Chart.js 支持的不同类型的刻度:用于径向图表(如雷达图和极坐标图)的线性刻度,以及用于散点图、气泡图、折线图和柱状图的笛卡尔刻度。你可以通过改变数据呈现方式或样式网格线和标签来为图表添加上下文来配置刻度。

我们还比较了不同类型的笛卡尔图表,说明了为什么有时使用对数刻度而不是线性刻度更好。我们创建了没有数值刻度的类别图表,并探讨了时间刻度。

在下一章中,我们将更详细地探讨配置,有效地使用颜色、字体和交互性,通过回调函数来控制图表的外观。

参考文献

书籍和网站

第六章:配置样式和交互性

在本章中,你将学习如何配置图表的外观和感觉,使其反映所需的布局或风格,遵循图表设计的良好实践,并调整其交互和响应行为。这包括配置颜色、渐变、图案和字体,设置边距、填充、边框、填充、背景、线宽、虚线,定位标题和图例,以及配置过渡和动画的行为。其中一些属性可以通过 Chart.js 配置选项轻松配置,但其他属性则需要插件和扩展,这些内容也将在本章中介绍。

在本章中,你将学习以下内容:

  • 默认配置

  • 字体

  • 颜色、渐变、图案和阴影

  • 添加文本元素和标签

  • 交互、数据更新和动画

默认配置

在 Chart.js 中创建的每个图表都预先配置了默认属性。在创建新的图表实例时,你始终可以在options对象中覆盖这些属性,但你也可以通过直接在Chart.defaults对象中设置属性来覆盖所有或许多图表的属性。

例如,任何类型的图表的默认线张力是0.4。如果你想所有图表都只使用直线,并且刻度从零开始,你可以让所有页面加载一个defaults.js文件,该文件声明以下默认值:

Charts.defaults.global.elements.line.tension = 0;
Charts.defaults.scales.ticks.beginAtZero = true;

如果你只想在雷达图中使用曲线线,你可以使用以下方式覆盖所有雷达图表的属性(但不是任何其他类型的图表):

Charts.defaults.radar.elements.line.tension = 0.4;

然后,如果你有一个特定的折线图,你更愿意使用曲线线,你可以在创建图表实例时再次覆盖属性,使用其options配置对象:

const chart = new Chart("my-chart", {     type: 'line', data: {...},
   options: {
     elements: {
       line: {
         tension: 0
         //overrides Charts.defaults.global.elements.line.tension
       }
     }
   }
 });

一些选项甚至可以配置为图表中的特定数据集,例如线张力。如果你在datasets数组中的特定数据集使用lineTension: 0.3,则只有对应该数据集的线将显示新的张力:

datasets: [{
    data: [1,2,1],
    lineTension: 0.3
}]

顺序很重要,层次结构也是如此。在更具体的环境中设置的属性将几乎总是覆盖在更一般环境中设置的值。并且任何全局属性都应该在实例化图表之前设置。在下一节中,我们将探讨可以在不同配置级别定义的选项,它们的对象结构和它们的默认值。

全局默认值

Chart.defaults.global中的属性包含所有类型图表的配置选项,包括图形元素、标题和副标题、布局属性、动画、工具提示、事件和插件。但它不包括网格和刻度,这些在Chart.defaults.scale对象中配置。Chart.defaults.global中可用的选项如下。所有这些属性(除默认字体和颜色设置外)也作为任何图表实例的选项配置对象中的属性:

对象 描述
defaultColor CSS 颜色 所有图表元素的默认颜色。此属性在多个图表元素中被覆盖,因此实际上并不太有用。默认值为 'rgba(0,0,0,0.1)'
defaultFontColor CSS 颜色(例如:'lightblue''#9cf''#ff0000''rgb(100%,50%,25%)''hsl(60,100%,50%)''rgba(100%,50%,25%,0.6)''hsla(60,100%,50%,0.1)' 所有文本的默认颜色(除非用更具体的字体颜色属性覆盖)。默认值为 '#666'
defaultFontFamily CSS 字体家族名称或列表(例如:'Helvetica'"Helvetica Neue"sans-serif' 所有文本的默认家族(除非用更具体的字体颜色属性覆盖)。默认值为 'Helvetica Neue''Helvetica''Arial'sans-serif'
defaultFontSize 像素字体大小 所有文本的默认像素大小(除非用更具体的字体大小属性覆盖)。默认值为 12。
defaultFontStyle CSS 字体样式(例如:'bold''italic''normal')或正在使用的字体中可用的任何样式(例如:'condensed bold''light',等等) 所有文本的默认样式(除非用更具体的字体样式属性覆盖)。默认值为 'normal'
layout.padding 用于 topleftrightbottom 的数字或具有数值属性的对象 如果值是数字,则在图表的所有边上应用像素大小的填充。如果是对象,则可以分别应用于图表的不同边。
maintainAspectRatio truefalse 保持画布元素的宽高比。
responsive truefalse 当画布大小改变时,调整图表大小。默认值为 true
showLines truefalse 如果为 true,则在点值之间显示线条。默认值为 true,但在散点图中会被覆盖为 false
title 对象 请参阅本章的图例和标签部分。
legend 对象 请参阅本章的图例和标签部分。
tooltips 对象 请参阅第七章,高级 Chart.js
hover 对象 请参阅第七章高级 Chart.js*。
elements 对象 请参阅本章的图表元素部分。
events 对象 请参阅本章的动画部分。
plugins 对象 请参阅第七章,高级 Chart.js
animation 对象 请参阅本章的动画部分。

在 Charts.defaults.global 中可配置的选项,可用于所有图表。回调函数未列出。

例如,以下配置将关闭任何图表之间的线渲染。由于此属性在折线图或雷达图中没有被覆盖,如果你创建一个折线图,它将没有线条。只有点将可见:

Chart.defaults.global.showLines = false;

此配置将关闭所有图表的图例(对于单数据集图表非常有用):

Chart.defaults.global.legend.display = false;

刻度默认值

可以在 Charts.defaults.scale 对象中全局配置刻度和网格。以下表格列出了该对象的高级属性和一些默认属性。其中一些默认属性在特定图表中被重写。在这些情况下,在此上下文中更改它们可能没有任何效果:

对象 描述 默认属性
display 显示(true)或隐藏(false)此图表的刻度。默认为 true
offset 向图表的左右边缘添加额外空间。默认为 false(在柱状图中被重写为 true)。
gridLines 所有刻度(某些在特定图表类型中被重写)的默认属性和回调函数。见第五章,刻度和网格配置
display = true
color = 'rgba(0,0,0,0.1)
lineWidth = 1
drawTicks: true
drawOnChartArea = true
offsetGridLines = false

|

scaleLabel 所有刻度的默认属性和回调函数(某些在特定图表类型中被重写)。有关详细信息,请参阅第五章,刻度和网格配置
display = false,
labelString = ''
lineHeight = 1.2
padding: {top: 4, bottom: 4}

|

ticks 所有刻度的默认属性和回调函数(某些在特定图表类型中被重写)。有关详细信息,请参阅第五章,刻度和网格配置
display = true
beginAtZero = false
autoSkip = true
reverse = false

|

在 Charts.defaults.scales 中可以配置的选项,用于配置所有图表的刻度。

例如,以下代码将在使用笛卡尔坐标轴的任何图表的所有轴上放置相同的标签(barhorizontalBarlinescatterbubble):

Chart.defaults.scale.scaleLabel.display = true;
Chart.defaults.scale.scaleLabel.labelString = 'default';

display 属性在所有刻度组件中可用。使用 display: false,您可以全局移除刻度、标签、网格线和其他非图表信息,仅在必要时在特定图表中重写这些属性。这是一个好习惯,并将最大化您图表的数据到墨水比率。

本节中的代码片段来自 Config/defaults-1-global-config.html,位于本章的 GitHub 仓库中。

图形元素

图形元素是用于在不同类型的图表中渲染数据集的可视化的基本元素。您可以通过配置 Chart.defaults.global.elements 上下文中的四个对象来为它们定义默认值,如下所示。其中一些属性在特定图表的默认值中被重写,因此在此级别更改它们可能不会产生任何效果:

对象 描述 默认属性
arc Canvas 弧的默认属性,用于饼图、环形图和极坐标面积图。
backgroundColor:"rgba(0,0,0,0.1)"
borderColor:"#fff"
borderWidth:2

|

line 用于线图和雷达图的 Canvas 线的默认属性。在 borderCapStyleborderJoinStyle 中使用了 Canvas 属性。有关在 fill 策略中使用的 Canvas 属性,请参阅第二章,技术基础。有关 fill 策略,请参阅第四章,创建图表
backgroundColor:"rgba(0,0,0,0.1)"
borderCapStyle:"butt"
//(see Chapter 2)
 borderColor:"rgba(0,0,0,0.1)"
 borderDash:[]
 borderDashOffset:0
 borderJoinStyle:"miter"
//(see Chapter 2)
 borderWidth:3
 capBezierPoints:true
 fill:true //(see Chapter 4)
 tension:0.4
 stepped: false

|

Point 值点实际上是使用 Canvas 弧线绘制的圆。此对象包含线、雷达、散点或气泡图中点的默认属性。有关更多点样式,请参阅第四章,创建图表
backgroundColor:"rgba(0,0,0,0.1)"
borderColor:"rgba(0,0,0,0.1)"
borderWidth:1
hitRadius:1
hoverBorderWidth:1
hoverRadius:4
pointStyle:"circle"
//(see Chapter 4)
radius:3

|

Rectangle 用于柱状图和 horizontalBar 图的 Canvas 矩形的默认属性。其中一个边框未绘制(跳过)。
backgroundColor:"rgba(0,0,0,0.1)"
borderColor:"rgba(0,0,0,0.1)"
borderSkipped:"bottom" ("left"in horizontalBar)
borderWidth:0

|

Charts.defaults.global.elements 中适用于所有图表元素的可选设置

以下代码将使所有线图和雷达图使用红色虚线 5 像素线作为默认值,除非在它们的默认配置或 options 对象中覆盖。请参阅 Config/defaults-2-global-elements.html,如下所示:

const line = Chart.defaults.global.elements.line;
line.borderDash = [5,5];
line.borderWidth = 5;
line.borderColor = 'red';

图表默认值

Chart.default 上下文中包含一个对象,用于 Chart.js 支持的每种图表类型。类型如下表所示,其中还列出了一些之前在每个图表中配置的属性。这些属性可以覆盖全局默认的图表样式和元素(Chart.default.global)以及刻度默认属性(Chart.default.scales)。您可以通过在 Chart.default 上下文中更改这些属性或局部在选项对象内部来定义新的默认值:

对象 描述 默认属性(选择)
bar 柱状图默认属性
hover.mode = 'label'
scales.xAxes[0].type = 'category'
scales.yAxes[0].type = 'linear'

|

horizontalBar 水平柱状图默认属性
hover.mode = 'index'
scales.xAxes[0].type = 'linear'
scales.yAxes[0].type = 'category'
elements.rectangle.borderSkipped = 'left'

|

pie 饼图默认属性和回调函数
circumference = 2 * Math.PI
cutoutPercentage = 0
hover.mode = 'single'

|

doughnut 饼图默认属性和回调函数
circumference = 2 * Math.PI
cutoutPercentage = 50
hover.mode = 'single'

|

line 线图默认属性和回调函数
hover.mode = 'label'
scales.xAxes[0] = {type: 'category', id: 'x-axis-0'}
scales.yAxes[0] = {type: 'linear', id: 'y-axis-0'}
showLines = true,
spanGaps = false

|

radar 雷达图默认属性
elements.line.tension = 0
scale.type = 'radialLinear'

|

polarArea 极坐标图默认属性
angleLines.display = false
gridLines.circular = true
pointLabels.display = false
ticks.beginAtZero = true
type = "radialLinear"
startAngle = Math.PI / 2

|

scatter 散点图默认属性
hover.mode = 'single'
showLines = false
scales.xAxes[0] = {type: 'linear', id: 'x-axis-1'}
scales.yAxes[0] = {type: 'linear', id: 'y-axis-1'}

|

bubble 气泡图默认属性
hover.mode = 'single'
scales.xAxes[0] = {type: 'linear', id: 'x-axis-0'}
scales.yAxes[0] = {type: 'linear', id: 'y-axis-0'}

|

Charts.defaults 中不同类型图表的默认选项

您可以通过将它们打印到 JavaScript 控制台并检查对象树来查看当前默认属性的值,使用 console.log()。以下代码将打印上下文根:

console.log(Chart.defaults);

您还可以检查(并修改)图表实例的属性。在这种情况下,您需要将新图表分配给一个变量句柄(请参阅 Config/defaults-1-global-config.html):

const chart = new Chart(…);
console.log("Chart Data, chart.config.data);
console.log("Chart Options, chart.options);

字体

Chart.js 使用 Canvas 来选择和显示本地和已安装的字体。字体配置涉及设置最多四个字体属性:家族、大小、样式和颜色。一个 fontFamily 是包含字体家族名称列表的字符串,一个 fontStyle 包含对应字体家族支持样式的名称,一个 fontColor 是任何有效的 CSS 兼容颜色字符串,而 fontSize 是表示像素大小的数字。您可以在包含文本的任何对象中配置字体属性:标题、刻度标签、图例标签或工具提示,或者您可以设置全局默认值,这些默认值将被显式设置字体属性的文字元素继承。

选择标准字体

基本字体属性命名为 fontFamilyfontSizefontStylefontColor。一些对象有这些相同属性的修饰版本。以下列出了这些对象:

包含文本元素的对象 描述 字体属性
Chart.defaults.global 全局默认值 defaultFontFamilydefaultFontSizedefaultFontStyledefaultFontColor

| Chart.defaults.global.title | 图表标题 | fontFamilyfontSize

fontStyle,

fontColor |

Chart.defaults.scale.ticks 轴标签
Chart.defaults.scale.ticks.minor 次要刻度标签
Chart.defaults.scale.ticks.major 主要刻度标签
Chart.defaults.global.legend 图例标签
Chart.defaults.global.tooltips 工具提示标题
Chart.defaults.global.tooltips 工具提示正文
Chart.defaults.global.tooltips 工具提示页脚

拥有字体配置属性的对象

由于通常避免在整个图表中使用多个字体家族是一个好的实践,因此全局选项是配置此属性的最好位置。您还可以设置其他字体默认值:

Chart.defaults.global.defaultFontFamily =
     'Courier, "Courier New", "Lucida Console", monospace';Chart.defaults.global.defaultFontSize = 12;
Chart.defaults.global.defaultFontStyle= 'normal';
Chart.defaults.global.defaultFontColor = '#333';

您始终可以选择在适当的地方覆盖特定属性,例如图表标题的字体大小:

Chart.defaults.global.title.fontSize = 24;

如果需要,您还可以再次覆盖它,在图表实例中设置不同的值:

const chart = new Chart("my-chart", {type: 'line', data: {…},
    options: {
        title: {
            display: true,
            text: "Very large title that doesn't fit in the default space",
            fontSize: 20
           }
        }
});

使用 Web 字体

Chart.js 可以使用您网站上的任何字体。除了所有浏览器都支持的常规字体(serifsans-serifmonospace)之外,您还可以使用通过样式表加载的在线字体。

在以下示例中,我们正在使用一个免费的网络字体(OFL 许可证),称为 Yanone Kaffeesatz,从 Google Fonts 获取。要安装它,只需将以下链接添加到您的 HTML 页面的 <head> 中:

<link href="https://fonts.googleapis.com/css?family=Yanone+Kaffeesatz"
       rel="stylesheet">

现在,您可以在 CSS 和 HTML 中使用 Yanone Kaffeesatz 字体家族。Canvas 可以通过使用 font 属性将其设置为上下文字体。在 Chart.js 中使用它的最简单方法是将其声明为默认全局字体。如果此功能可用,您还可以配置任何字体样式:

Chart.defaults.global.defaultFontFamily = '"Yanone Kaffesatz", sans-serif';

在以下示例中,我们更改了几个字体属性(familycolorsizestyle),使用了上一章中创建的一个饼图/环形图的全球选项:

Chart.defaults.global.defaultFontColor = 'black';
Chart.defaults.global.defaultFontFamily =
      '"Yanone Kaffesatz", "Helvetica Narrow", "Arial Narrow", sans-serif';
Chart.defaults.global.defaultFontSize = 24;
Chart.defaults.global.defaultFontStyle = 'normal';
Chart.defaults.global.title.fontSize = 40;
Chart.defaults.global.title.fontColor = 'hsla(240,50%,70%,1)';
Chart.defaults.global.legend.labels.fontColor = 'hsla(120,20%,60%,1)';

结果在以下图表中显示。代码位于Fonts/fonts-1.html,并需要安装 Yanone Kaffesatz 字体(或任何其他字体,如果你编辑代码):

使用网络字体进行标题和标签的环形图。代码:Fonts/fonts-1.html.

颜色、渐变、图案和阴影

选择有效的数据可视化配色方案并非易事。颜色不仅仅用于使图表看起来更美观。除了区分和暗示数据集之间的关联外,它们还可能通过色调、对比度、饱和度或亮度等属性传达信息。它们甚至可以影响观看者的情绪。颜色的选择永远不会是中立的。它可能会吸引或排斥观看者对相关信息。

其他方面可能也很重要,具体取决于你的受众。你可能希望出于纯粹的美学原因使用渐变、斜面和阴影,但如果你的受众需要最大程度的可访问性,你也可能需要考虑使用色盲安全调色板或图案。

配置颜色

Chart.js 支持标准的 HTML/CSS 颜色名称和代码(见第二章,技术基础),这些名称和代码被分配给控制字体、线条(和边框)、填充的属性。你可以通过名称(例如,red)选择颜色,十六进制代码(#f00#ff0000),或者接收 RGB 或 HSL 组件的三参数生成函数(rgb(255,0,0),或者hsl(0,100%,50%))。CSS 颜色生成函数还包括一个四参数版本,它使用 alpha 组件控制透明度('rgba(255,0,0,1)',或者'hsla(0,100%,50%,1)')。

Chart.defaults.global.defaultColor属性为所有图表组件设置默认颜色,但它通常被字体、刻度、图形元素和默认为单色调的图表的默认配置所覆盖。这些属性出现在不同的图表元素中。它们有不同的名称,但它们都以Color后缀结尾。基本配置属性(在Chart.defaults.globaloptions对象中使用)包含一个颜色,但应用到一个单一数据集时,它们也可以是一个颜色数组。

配色方案和调色板

Chart.js 不包含原生的调色板生成器。在我们之前的例子中,我们要么指定了显式颜色,要么创建了不超过六色的调色板,或者使用了随机颜色生成函数。但颜色是图表中传达信息的重要手段,应该仔细选择。如果不小心使用,您的图表可能会暗示数据之间不存在的关联,欺骗观众。明暗和饱和度不同的颜色暗示着顺序关系(更强/更弱或更热/更冷)。对立的数据可以使用发散调色板更好地表示,其中极端值由互补颜色表示。如果您的数据代表不同的类别,则使用定性颜色方案将更好地可视化。根据您的受众和图表的目的,您在选择颜色时还可能需要考虑可访问性问题,例如色盲或颜色受限设备的渲染。所有这些任务都可以通过使用专门设计的调色板或方案来简化。

调色板是一系列固定大小的颜色,通常在 JavaScript 中表示为数组。一个 方案 代表一系列调色板,通常在 JavaScript 中是一个函数(或一个对象)。您可以使用方案生成包含任意颜色序列的调色板。

您可以编写自己的调色板、方案和颜色生成器,但使用流行的服务和 JavaScript 库生成精心选择的调色板和方案要容易得多。

ColorBrewer 是由 Cynthia Brewer 创建的网站,您可以在其中生成一个包含精心设计的调色板的数组字符串,这些调色板不仅看起来很漂亮,而且还考虑了您使用的数据类型(定性、发散和顺序)及其可访问性(色盲、显示/打印和灰度)。您可以选择并实时查看效果,配置可访问性和数据属性,并以不同格式(包括 JavaScript 数组和 CSS)生成颜色字符串:

使用 ColorBrewer 选择和生成一个颜色盲安全的小调色板

让我们用一个简单的条形图来试试,该条形图包含代码中列出的单个数据集,如下所示:

<body>
<canvas id="canvas" width="200" height="100"></canvas>
<script>
    const data = {
        labels: ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat"],
        datasets: [
            {
                data: [10, 5, 2, 20, 30, 41],
            }
        ]
    };
    new Chart('canvas', { type: 'bar', data: data, 
                          options: {legend: {display: false}} });
</script>
</body>

当您加载页面时,它应该显示一个单色条形图,其中所有条形共享相同的灰色调。

使用 ColorBrewer 网站,选择一个六色调色板,配置您想要的任何属性,然后将 JavaScript 数组复制到您的剪贴板。将其粘贴为数据集的 backGroundColor 属性:

datasets: [{
   data: [10, 5, 2, 20, 30, 41],
   backgroundColor:['#d73027','#fc8d59','#fee090','#e0f3f8','#91bfdb','#4575b4']
}]

然后加载您的图表并查看结果。它应该类似于以下条形图。完整代码在 Colors/colors-1-brewer.html

使用 ColorBrewer 六色发散调色板的颜色进行图表。代码:Colors/colors-1-brewer.html.

ColorBrewer 调色板限制为九种颜色(或根据您选择的设置更少),如果您需要更多颜色,可以从 保罗·托尔的方案页面 中选择,该页面也非常受欢迎,或者使用其他生成器(有很多)。

另一个选项是使用 Google palette.js 库,它包含颜色调色板生成函数。它支持 ColorBrewer 和 保罗·托尔色彩方案页面 中的所有方案,并包括 HSV、RGB 和 Solarized 方案的附加生成器。要使用它,您需要在页面上包含 palette.js 文件。您可以从 GitHub 网站下载它或使用 CDN:

<script src="img/palette.min.js"></script>

现在您可以通过调用位于 google.github.io/palette.js 的演示页面中列出的某个颜色方案函数来生成调色板,如下所示:

图片

包含 palette.js 生成器支持的色彩方案的页面列表(请参阅完整的演示页面 google.github.io/palette.js)

演示页面允许您尝试不同的方案,检查您可以在调色板中包含多少颜色,并模拟不同级别的色盲。以下代码将生成包含保罗·托尔定性色彩方案的六个颜色的调色板,用于我们的条形图:

const colorsArray = palette('tol', 6);

颜色数组包含颜色的十六进制代码,但 Canvas(和 Chart.js)不会显示颜色,除非数字前有一个哈希字符。以下代码修复了这个问题:

const colorsArray = palette('tol', 6).map(n=>'#'+n);

现在只需将 backgroundColor 属性设置为颜色数组:

backgroundColor: colorsArray

结果如下所示。代码在 Colors/colors-2-palettejs.html 中:

图片

使用生成调色板中的颜色创建的图表。代码:Colors/colors-2-palettejs.html

渐变

Chart.js 没有对渐变的原生支持,但它们使用 Canvas 生成相当容易。问题是梯度在 Canvas 对象中有一个绝对位置,而您的图表可能是响应式的。如果图表被调整大小,梯度必须重新计算,并且图表需要更新。

处理这个问题的一种方法是在图表创建时以及每次窗口调整大小时调用一个渐变函数,将渐变将应用于的区域尺寸传递给 Canvas 渐变函数。我们可以使用回调和 Chart.js 的 update() 函数来完成此操作。

在 Canvas 中创建渐变的函数如下:

canvasContext.createLinearGradient(x0, y0, x1, y1);

梯度包含一条垂线的方程。要创建沿 y 轴变化的线性梯度,我们需要从图表的底部画到顶部。这意味着 x0 = x1 = 0y1 是图表的底部,而 y0 是顶部。如果我们编写一个接收图表实例的函数,我们可以从 scales["y-axis-0"].topscales["y-axis-0"].bottom 中检索这些信息。以下是一个用于绘制背景颜色和包含两个数据集的折线图的梯度函数(Colors/colors-3-gradient.html):

function drawGradient(chart) {
     const x0 = 0;
     const y0 = chart.scales["y-axis-0"].top;
     const x1 = 0;
     const y1 = chart.scales["y-axis-0"].bottom;

     const gradient1 = chart.ctx.createLinearGradient(x0, y0, x1, y1);
     gradient1.addColorStop(0, 'hsla(60,100%,70%,.4)');
     gradient1.addColorStop(1, 'hsla(0,100%,25%,.8)');

     const gradient2 = chart.ctx.createLinearGradient(x0, y0, x1, y1);
     gradient2.addColorStop(0, 'hsla(300,100%,70%,.4)');
     gradient2.addColorStop(1, 'hsla(240,100%,25%,.8)');

     chart.data.datasets[0].backgroundColor = gradient1;
     chart.data.datasets[1].backgroundColor = gradient2;
 }

你必须在图表创建后立即调用该函数,然后调用 update() 重新绘制图表。每次调整大小后,都要再次调用它。这可以通过 onComplete() 动画回调自动完成,如下面的代码所示:

const data = {
    labels: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"],
    datasets: [
        {
            label: 'Week 1',
            data: [2, 5, 2, 0, 20, 48, 51],
            borderColor: 'red'
        },{
            label: 'Week 2',
            data: [44, 36, 13, 40, 40, 9, 3],
            borderColor: 'blue'
        }
    ]
};

const chart = new Chart('canvas', {
    type: 'line',
    data: data,
    options: {
        animation: {
            onComplete: function(context) {
               drawGradient(context.chart);
            }
        }
    }
});
drawGradient(chart);
chart.update();

最终结果如下所示:

使用渐变作为每个数据集的背景颜色的折线图。代码:Colors/colors-3-gradient.html.

图案

图案是创建不依赖于颜色编码的图表的绝佳方式,并且它们可以在彩色或单色设备或印刷媒体中使用。当然,它们也是色盲安全的。你可以使用类似于用于渐变的 HTML Canvas 命令来创建图案,但使用插件,如 Chart.js 官方文档中列出的 Patternomaly 插件,要容易得多。

你可以通过从其 GitHub 仓库 (github.com/ashiguruma/patternomaly) 下载 JavaScript 库或使用 CDN 链接来获取 Patternomaly:

<script src="img/patternomaly.min.js">
</script>

要生成一个图案,你只需选择一个颜色并调用 pattern.generate(),它将随机选择 21 个可用图案中的 1 个:

pattern.generate('rgb(50%,20%,10%');

你还可以将特定图案作为 pattern.draw() 的第一个参数选择:

pattern.draw('triangle', 'lightblue');

支持的图案列表如下所示 (Colors/colors-4-patternomaly.html):

patternomaly.js 插件中可用的图案。代码:Colors/colors-4-patternomaly.html.

generate() 函数也接受一个颜色数组作为参数。你可以包含为 Color Brewer 示例获取的调色板,并基于它们生成图案:

let patternArray = ['#d73027','#fc8d59','#fee090','#e0f3f8','#91bfdb','#4575b4'];
pattern.generate(patternArray);

让我们使用图案来为条形图着色。在这个例子 (Colors/colors-5-pattern.html) 中,我们将从 palette.js 库(它返回一个颜色数组)的 pallete() 函数调用传递给 generate() 作为参数,并将其分配给条形的 backgroundColor 属性:

backgroundColor: pattern.generate( palette('tol', 6).map(n=>'#'+n) ),

结果如下所示:

使用生成的图案和颜色创建的无色盲图表。代码:Colors/colors-5-pattern.html.

阴影和斜面

在官方 Chart.js 文档中列出了几个第三方插件,其中之一名为 chartjs-plugin-style,为图表添加了一些样式选项;这些选项包括斜面、阴影和发光效果。要使用它,你可以通过 npm 安装插件或从 nagix.github.io/chartjs-plugin-style 下载 JavaScript 文件,并将其导入到你的页面中:

<script src="img/chartjs-plugin-style.min.js"></script>

现在,你可以在你的数据集中使用新的属性来添加斜面、阴影和发光效果。以下示例配置了斜面并为简单的条形图添加了阴影。属性的名称应该是自解释的:

const data = {
    labels: ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat"],
    datasets: [
        {
            label: 'Week 1',
            data: [20, 5, 2, 20, 30, 51],
            backgroundColor: ['yellow','red','blue','green','orange', 
            'cyan'],
            bevelWidth: 3,
            bevelHighlightColor: 'rgba(255, 255, 255, 0.75)',
            bevelShadowColor: 'rgba(0, 0, 0, 0.5)',
            shadowOffsetX: 5,
            shadowOffsetY: 5,
            shadowBlur: 10,
            shadowColor: 'rgba(0, 0, 0, 0.5)',
        }
    ]
};

new Chart('canvas', { type: 'bar', data: data,
                      options: {legend: {display: false}} });

最终结果将在下面展示。您还可以将其与生成的颜色、调色板函数和图案混合。尝试使用不同类型的图表!代码位于 Colors/colors-6-shadows.html

一个带有斜面和阴影的条形图。代码:Colors/colors-6-shadows.html.

添加文本元素和标签

您始终可以在图表外部添加标题和副标题,使用纯 HTML 或 JavaScript。然而,Chart.js 还包括绘制和配置图表内部文本元素的属性,作为图表的一部分。如果属性不足以提供足够的灵活性,您可以使用回调来过滤或生成标签。如果工具提示、标题和图例还不够,您还可以使用插件向条形图、切片和线条添加标签。您甚至可以使用纯 HTML Canvas 在图表上绘制。本节将探讨一些这些技术。

图例和标签

默认情况下,图例在条形图、折线图、饼图和环形图中显示。它们显示为与数据集表示的线条、条形或切片颜色相关的标签化、彩色框列表,即使在只有一个数据集的情况下也会在屏幕上渲染。在这种情况下,您可能希望隐藏它们。您还可以调整其他几个属性和回调。最重要的属性如下列所示:

属性 描述
显示 truefalse 显示或隐藏图表的图例。默认为 true
位置 'top', 'bottom', 'left', 'right' 选择标签相对于图表的位置。默认为 'top'
反转 truefalse 反转图例中标签的顺序。默认为 false
标签 对象 配置每个标签的文本和彩色框。

图例对象的主要属性

您还可以将两个回调附加到图例上:

属性 参数 描述
onClick (event,label):label.text 属性包含标签的文本;label.datasetIndex 包含数组的索引。 'click' 事件做出反应。默认实现切换标签和相关数据集的显示与隐藏。
onHover (event,label):label.text 属性包含标签的文本;label.datasetIndex 包含数组的索引。 'hover' 事件做出反应。默认情况下没有实现此回调。

图例对象的回调函数

以下示例包含一个简单的三数据集折线图。不是隐藏数据集,而是覆盖了图例的 onClick 回调,将所选数据集的颜色更改为灰色。请注意,数据集索引是从回调参数中获得的,但数据集属性是在当前图表的对象树中更改的(this.chart.data.datasets):

const data = [[12,19,3,5,2,3],[6,5,33,2,7,11],[2,3,5,16,0,1]],
   strokes = ['rgba(54,162,235,1)','rgba(255,99,132,1)', 
              'rgba(132,255,99,1)'],
   fills =  
   ['rgba(54,162,235,.2)','rgba(255,99,132,.2)','rgba(132,200,99,.2)']; const grayFill = 'rgb(0,0,0,.2)';
const grayStroke = 'rgb(0,0,0,.8)';

const datasets = [];
for(const i = 0; i < data.length; i++) {
     datasets.push({
         label: 'Dataset ' + (i+1),
         data: data[i],
         backgroundColor: fills[i],
         borderColor: strokes[i],
         borderWidth: 1
     });
 }

 const myChart = new Chart("myChart", {
     type: 'line',
     data: {
         labels: ['Day 1','Day 2','Day 3','Day 4','Day 5','Day 6'],
         datasets: datasets,
     },
     options: {
         legend: {
             position: 'left',
             reverse: true,
             onClick: function(event, label) {
                 const index = label.datasetIndex;
                 const dataset = this.chart.data.datasets[index];
                 if(dataset.backgroundColor == fills[index]) {
                     dataset.backgroundColor = grayFill;
                     dataset.borderColor = grayStroke;
                 } else {
                     dataset.backgroundColor = fills[index];
                     dataset.borderColor = strokes[index];
                 }
                 this.chart.update();
             }
         }
     }
 })

以下截图显示了点击数据集前后的图表。请参阅完整的代码 Text/text-1-legend-callback.html

实现一个 onClick 回调以更改数据集的颜色。代码:Text/text-1-legend-callback.html.

legend.labels 属性用于配置单个图例标签的显示外观。以下表格显示了您最可能使用的属性:

属性 描述
fontSize, fontStyle, fontColor, fontFamily 数字和字符串 字体属性继承全局字体设置。
boxWidth 数字 彩色框的宽度。默认值为 40。
Padding 数字 彩色框行之间的填充。

图例.labels 对象的主要属性

没有属性可以设置彩色框的颜色。如果没有为数据集分配颜色,它通常会继承全局的 defaultColor。您可以使用 generateLabels 回调属性更改此行为。您还可以通过将函数分配给 filter callback 属性来过滤掉不需要的标签。以下列出了这些属性:

属性 参数 描述
generateLabels (chart): 当前图表。这与 this.chart 相同。 默认实现返回数据集标签作为文本和一个与数据集颜色匹配的矩形彩色框。
filter (label, item): label.text 包含标签的文本;label.datasetIndex 包含数组索引;item.datasets 包含数据集数组;item.labels 包含 x 轴标签或饼图切片标签。 这包含一个过滤函数,用于返回应显示的标签。默认实现返回 true。此属性仅过滤标签,不过滤数据集(线条或切片仍会显示)。

图例.labels 对象的回调属性

标签样式可以在 options 对象内部配置,在每个图表实例中配置,或者使用 Global.defaults.legend 对象为所有图表配置,例如:

    Chart.defaults.global.legend.labels.fontSize = 16;
    Chart.defaults.global.legend.labels.boxWidth = 20;

以下过滤配置将仅显示最大值低于 20 的数据集的标签。所有三个数据集都将显示,但只有两个标签将显示(Text/text-2-legend-label.html):

labels: {
    filter: function(label, item) {
      return Math.max(...item.datasets[label.datasetIndex].data) <= 20;
    }
}

generateLabels 回调应该仅在您想创建自己的图例时实现。如果您有一个非常复杂的图例,可以通过实现 Chart.defaults.global.legendCallback 属性的回调函数或在每个图表中使用 options 中的 legendCallback 属性来生成 HTML 图例。这将在 第七章, 高级 Chart.js* 中探讨。

标题

Chart.js 的默认设置是不开启标题,因为您也可以在 HTML 中以更大的灵活性创建自己的标题。如果您仍然想在图表中添加标题,您至少需要设置两个属性:display(值为 true)和 text(标题文本)。您可能还想配置的其他属性如下:

属性 描述
display truefalse 显示标题。默认为 false
text StringString[] 包含标题文本的字符串或字符串数组,用于多行标题。
fontStylefontFamilyfontSizefontColor StringNumber 字体属性。默认 fontStyle 为粗体,但其他属性继承。
lineHeight Number 默认行高为 1.2。
padding Number 默认填充为 10。
position 'top''bottom''left''right' 这是标题应该放置的位置。默认为 'top'。标题在侧面的位置将逆时针旋转 90 度。

[options.title 对象的主要属性]

您可以使用 Chart.defaults.global.title 为所有图表配置标题,或在新的图表实例的 options 对象中配置。您也可以在更新图表或响应事件后随时更改标题。

向线条、条形和切片添加标签

在 第四章 创建图表 中,我们使用了一个简单的插件向饼图切片添加标签。在本节中,我们将向您展示另外两个允许更多定制的插件。它们列在 Chart.js 的官方文档中,但由第三方开发,应从它们自己的仓库安装或下载。

chart-plugin-datalabels 插件提供了对所有类型图表中值的高自定义标签功能,支持脚本和事件处理。您可以在 chartjs-plugin-datalabels.netlify.com/samples/ 中看到几个示例,那里也有文档链接和 GitHub 仓库链接。安装它的最简单方法是使用 CDN。在您的页面中包含以下代码:

 <script src="img/chartjs-plugin-datalabels">  
 </script>

可以通过以下三种上下文中的一种来按数据集、按图表或全局进行配置:

  • 在数据集中:dataset.datalabels.*

  • 在图表实例中:options.plugins.datalabels.*

  • 全局,对所有图表:Chart.defaults.global.plugins.datalabels.*

本地设置会覆盖全局设置。详细内容超出了本章范围,但该插件有很好的文档记录。以下是一个简单示例,使用我们在上一节中使用的折线图。所有配置都是在 options.plugins.datalabels 对象中完成的,该对象在数据点上方添加了漂亮的标签(见 Text/text-4-datalabels.html):

options: {
    plugins: {
        datalabels: {
            backgroundColor: function(context) {
                return context.dataset.borderColor;
           },
          borderRadius: 4,
          color: 'white',
          font: { weight: 'bold'},
          formatter: Math.round
     }
 },
}

结果如下截图所示:

使用 chartjs-datalabels 插件向折线图添加值标签。代码:代码位于 Text/text-4-datalabels.html

使用此插件还有更多可以做的事情。在其他图表上尝试它并查看示例。

第二个标签插件是 chart-plugin-outlabels. 它允许在饼图和环形图中更好地可视化数据值,将标签显示在切片外部。您可以在 piechart-outlabels.netlify.com/sample/ 中看到一个示例,在那里您还可以找到文档和 GitHub 仓库的链接。要在您的页面上使用它,请包含以下内容:

<script src="img/> outlabels"> </script>

如许多其他插件一样,可以通过以下三种上下文中的一种来按数据集、按图表或全局进行配置:

  • 在数据集中:dataset.outlabels.*

  • 在图表实例中:options.plugins.outlabels.*

  • 对于所有图表全局:Chart.defaults.global.plugins.outlabels.*

该插件还引入了一种新的图表类型:outlabeledPie。它可以替代 piedoughnut,并且配置起来更简单。

以下是我们之前示例中使用的 doughnut 图表的简单示例,使用 outlabeledPie(完整代码请见 Text/text-5-outlabels.html):

 const data = {
     labels: ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat"],
     datasets: [
         {
             label: 'Week 1',
             data: [20, 5, 2, 20, 30, 51],
             backgroundColor: palette('tol', 6).map(n=>'#'+n),
         }
     ]
 };

 new Chart('canvas', {
     type: 'outlabeledPie',
     data: data,
     options: {
         zoomOutPercentage: 30,
         plugins: {
             legend: false,
             outlabels: {
                 text: '%l %p',
                 color: 'white',
                 stretch: 45,
                 font: {
                     resizable: true,
                     minSize: 12,
                     maxSize: 18
                 }
             }
         }
     }
 });

结果如下所示:

图片

使用 chartjs-outlabels 插件标记值的饼图。代码:Text/text-5-outlabels.html

查看文档以获取更多选项,并尝试在其他图表中使用此插件。

交互、数据更新和动画

除非更改配置选项,否则所有图表都预先配置了默认行为和过渡效果,这些效果是动画化的。动画由事件触发,例如窗口调整大小、数据更新或用户交互。默认预配置的图表交互包括将鼠标悬停在或靠近值点上(用于触发包含详细信息的工具提示的出现)以及点击或触摸图例。这提供了基本的交互性以及平滑的数据过渡,但您可能仍然希望通过编写不同事件或动画阶段的回调函数、更改动画属性(如持续时间或缓动算法)或甚至完全关闭动画来对其进行微调。如果您需要更多控制,您可以使用插件或标准 JavaScript 扩展大部分此功能。

数据更新

一个交互式图表可能显示周期性变化的数据。一个网页可能包含一个自动更改数据的算法;它可能下载包含新数据的新数据文件,或者它可能允许用户输入或请求更改源数据值。在任何这些情况下,一旦新数据可用,图表都应该更新。数据更新可以在回调函数内部自动发生,也可以使用 update() 命令显式调用。要使用它,您需要保存对图表对象的变量引用:

const chart = new Chart(…);
 // make changes
 chart.update();

当使用回调时,通常可以使用this关键字引用图表的当前实例:

callback: function() {
     // make changes
     this.chart.update();
}

变更通常涉及图表实例的datasetsoptions属性。让我们看一个例子。在以下代码中,square()函数将平方图表中的所有数据值并将x轴更改为对数刻度。squareRoot()函数执行相反的操作。在更新网格(使用未记录的scaleMerge()函数)后,图表被更新:

function square(chart) {
    const datasets = chart.config.data.datasets;
    for(let i = 0; i < datasets.length; i++) {
        for(let j = 0; j < datasets[i].data.length; j++) {
            let value = datasets[i].data[j];
            datasets[i].data[j] = value * value;
        }
    }
    chart.options.scales.yAxes = 
        Chart.helpers.scaleMerge(Chart.defaults.scale, 
                                 {yAxes: [{type: 'logarithmic'}]}).yAxes; 
    chart.update();
}

function squareRoot(chart) {
    const datasets = chart.config.data.datasets;
    for(let i = 0; i < datasets.length; i++) {
        for(let j = 0; j < datasets[i].data.length; j++) {
            let value = datasets[i].data[j];
            datasets[i].data[j] = Math.sqrt(value);
        }
    }
    chart.options.scales.yAxes = 
        Chart.helpers.scaleMerge(Chart.defaults.scale, 
                                 {yAxes: [{type: 'linear'}]}).yAxes; 
    chart.update();
}

HTML 按钮注册为事件监听器,根据当前y轴类型调用两个函数之一,并更新图表:

 let button = document.getElementById("toggle");
 button.addEventListener('click', function() {
     const type = myChart.options.scales.yAxes[0].type;
     if(type == 'linear') {
         square(myChart);
     } else {
         squareRoot(myChart);
     }
 });

尝试一下。完整代码在Animation/animation-1-update.html中,以下截图显示了图表的两个不同状态:

图片

更改值和刻度后更新图表。代码:Animation/animation-1-update.html.

事件

你可以通过局部配置options.events属性或全局使用Chart.defaults.global.events来选择你的图表将响应哪些事件。默认配置包括一个包含六个事件名称的数组:

events: ["mousemove", "mouseout", "click", "touchstart", "touchmove", "touchend"]

这些是当光标位于画布上下文内时浏览器将监听的事件。它们控制可点击项(如图例标签和工具提示)的行为。如果你正在编写自己的处理程序,你可能希望通过重新定义属性来包含包含较少事件的数组来关闭某些事件。例如,如果你想在一个图表中禁用悬停和触摸事件,只允许click事件,你可以在你的选项配置中添加以下内容:

options: {
   events: ['click']
}

配置动画

你应该已经注意到,当你点击按钮时,线条不会立即移动到它们的新位置。图表平滑过渡,大约需要一秒钟。通过调用update()触发的过渡将自动使用标准动画配置。

有两个动画属性你可以轻松更改。它们如下列出:

属性 描述
持续时间 Number 动画持续的时间(以毫秒为单位)。默认为 1,000(一秒)。
缓动 'linear', 'easeInQuad', 'easeOutQuad', 'easeInOutQuad', 'easeInCubic', 'easeOutCubic', 'easeInOutCubic', 'easeInQuart', 'easeOutQuart', 'easeInOutQuart', 'easeInQuint', 'easeOutQuint', 'easeInOutQuint', 'easeInSine', 'easeOutSine', 'easeInOutSine', 'easeInExpo', 'easeOutExpo', 'easeInOutExpo', 'easeInCirc', 'easeOutCirc', 'easeInOutCirc', 'easeInElastic', 'easeOutElastic', 'easeInOutElastic', 'easeInBack', 'easeOutBack', 'easeInOutBack', 'easeInBounce', 'easeOutBounce', 'easeInOutBounce' 用于动画的缓动函数。这些基于Robert Penner 的缓动函数robertpenner.com/easing)。如果你查看每个缓动函数的图形表示,选择起来会更简单,这些图形表示可在easings.net找到。

选项.animation 对象的属性

要立即过渡到新值(不进行任何动画),你应该包含一个包含duration:0的对象:

options: {
    animation: {
        duration: 0
    }
}

现在变化将立即发生。

属性可以在每个图表的选项对象中配置,或在Chart.defaults.global中全局配置。

有两个用于配置动画的callback属性,如下列出。一个允许你挂钩到动画的每个步骤,另一个允许动画完成后运行代码:

属性 参数 描述
onProgress (animation): 主要属性有animation.chart(当前图表)、animation.currentStepanimation.numStepscurrentStep/numSteps返回动画到目前为止的百分比) 在动画的每个步骤后调用。
onComplete (animation): 主要属性有animation.chart(当前图表)、animation.currentStepanimation.numStepscurrentStep/numSteps返回动画到目前为止的百分比) 在动画结束时调用。任何在图表渲染后要应用的变化(如 Canvas 覆盖)应在此上下文中调用。

选项.animation 的回调属性

我们在前面示例的网页中添加了一个 HTML 进度条,并在以下代码中将折线图动画设置为持续五秒。在每一步,进度条通过onProgress回调函数更新。每次回调都会在每次调用时将当前步骤打印到 JavaScript 控制台:

<body>
<canvas id="myChart" width="400" height="200"></canvas>
<form><button type="button" id="toggle">Square/Unsquare</button></form>
<progress id="progress" max="1" value="0"></progress>
<script>
    ...
    const progress = document.getElementById("progress");
    ...
    const myChart = new Chart("myChart", { type: 'line', data: {…},
        options: {
           animation: {
                 duration: 5000,
                onProgress: function(animation) {
                    console.log(animation.currentStep /  
                                animation.numSteps);
                    progress.value = animation.currentStep / 
                                     animation.numSteps;
                },
                onComplete: function(animation) {
                    console.log(animation.currentStep);
               }
            }
      }
})
    let button = document.getElementById("toggle");
    button.addEventListener('click', function() {…});
</script>

完整的代码在Animation/animation-2.html中。以下是动画进行到一半时的截图:

在更新图表后的五秒动画期间使用进度条。代码:Animation/animation-2.html

在这个示例中,onComplete 回调只是简单地打印到控制台,但如果在图表渲染到屏幕后需要更新或更改任何内容,它是最重要的回调之一。如果你在回调之外将某个东西绘制到 Canvas 上,Chart.js 将会擦除它。在第四章“创建图表”中,我们使用它通过 Canvas API 在饼图上绘制文本。在本章中,我们在每次调整大小事件后给图表添加了渐变色。

摘要

在本章中,我们探讨了使用 Chart.js 创建交互式图表的多种配置外观和感觉的方法,包括使用原生属性,以及一些扩展和插件。

我们首先学习了如何设置全局默认值,这些默认值可以被多个图表继承,并用于在不同图表之间设置一致的主题,共享基本的布局、字体和配色方案。我们还探索了一些在线服务、工具、扩展和插件,用于样式化图表和添加标签。然后我们配置了图表在更新和用户交互后的行为,调整动画算法和回调。

你已经掌握了足够的 Chart.js 知识来创建任何图表。在下一章中,我们将更深入地探讨一些这些主题,配置工具提示,学习如何编程 Chart.js API,你还将学习如何创建自己的插件。

参考文献

书籍和网站

数据来源

第七章:高级 Chart.js

当您使用 Chart.js 创建数据可视化时,您的大部分工作将涉及准备数据,以便它可以被图表实例加载和使用。您不必过多担心字体、内边距、坐标轴、屏幕调整大小或响应性,因为新图表预先配置了默认值,旨在实现最佳展示和交互性。在上一个章节中,我们学习了如何调整不同类型图表的颜色、标签、动画和其他典型配置。在本章中,我们将探讨您不太经常使用且可能需要额外编码、扩展以及与其他库(如提示信息行为配置、标签生成、脚本、创建混合图表、创建插件、使用 Chart.js API 和使用 HTML Canvas 与 Chart.js)集成的配置主题。

本章您将学习以下内容:

  • 提示信息配置

  • 高级图例配置

  • 显示多个图表

  • 扩展 Chart.js

提示信息配置

提示信息是 Chart.js 用于揭示数据定量细节的主要功能。虽然一些上下文来自网格,但要在数据点旁边原生显示数据,唯一的方法是使用提示信息。当然,您也可以像我们在上一章中看到的那样标记值点,但这需要扩展或插件,并且如果过度使用可能会使图表显得杂乱。Chart.js 可视化依赖于交互性来显示细节。在本节中,我们将学习如何配置这些细节的显示方式。

可以使用 options 对象中的 tooltips 键为每个图表配置提示信息。也可以使用 Chart.defaults.global.tooltips 为所有图表配置。您可以配置的这些对象的属性列在以下表格中:

对象 描述
titleSpacing 数字 每一行 标题 前后的空格。默认值是 2
bodySpacing 数字 每一个 提示信息 项目前后的空格。默认值是 2
footerSpacing 数字 每一行 页脚 前后的空格。默认值是 2
titleMarginBottom 数字 title 之后的边距(像素)。默认值是 6
footerMarginTop 数字 footer 之前的边距(像素)。默认值是 6
xPadding 数字 像素单位的垂直内边距。默认值是 6
yPadding 数字 像素单位的水平内边距。默认值是 6
enabled truefalse 打开或关闭提示信息。默认值是 true
intersect truefalse 如果为 true,则提示交互模式仅在光标正好悬停在点(在 pointHitRadius 内)上时应用。如果为 false,则始终应用。全局默认值是 true,但根据图表类型可能会有所变化。
mode nearest, index, dataset, x, y。已弃用的值是 label(与 index 相同),和 single(当 intersect: true 时,行为类似于 nearest)。 选择工具提示交互模式。nearest 显示最近点的值(每个工具提示包含一个项目),index 显示具有相同索引的所有点的值(每个 dataset 在同一个工具提示中都有一个项目),dataset 将整个 dataset 显示在工具提示中。仅对笛卡尔尺度提供两种其他模式:x 将包含所有具有相同 x 坐标值的项,y 将包含所有具有相同 y 坐标值的项。在笛卡尔尺度中,index 模式默认为 x 索引,也可以通过添加属性 axis: y 来设置 y 索引。全局默认值为 nearest,但根据图表类型而变化。
position average, nearest, 或自定义位置 定义工具提示相对于值点的位置。默认为 average。(您可以通过在 Chart.Tooltip.positioners 映射中创建一个返回包含 xy 坐标对象的条目来自定义自己的自定义位置。)
titleFontFamily, titleFontStyle, titleFontColor, titleFontSize StringNumber title 的字体属性(使用 callbacks 进行配置)。
bodyFontFamily, bodyFontStyle, bodyFontColor, bodyFontSize StringNumber body 的字体属性(使用 callbacks 进行配置)。
footerFontFamily, footerFontStyle, footerFontColor, footerFontSize StringNumber footer 的字体属性(使用 callbacks 进行配置)。
caretSize Number 工具提示箭头的大小(以像素为单位)。默认为 5
caretPadding Number 箭头尖端与工具提示位置(例如值点)的距离。默认为 2
cornerRadius Number 圆角矩形的半径(以像素为单位)。默认为 6
backgroundColor CSS 颜色 工具提示的背景颜色。默认为 rgba(0,0,0,0.8)
multiKeyBackground CSS 颜色 着色框的背景(如果 dataset 颜色不透明则不可见)。默认为 #fff
borderColor CSS 颜色 工具提示的边框颜色。默认为 rgba(0,0,0,0)
borderWidth Number 工具提示的边框宽度。默认为 0。
displayColors truefalse 如果为 false,则隐藏颜色框。默认为 true
callbacks Object 包含多个回调函数的对象。请参阅本章中工具提示 callbacksTooltip callbacks 部分。

工具提示的静态属性(用于 options.tooltips 键)

在以下示例中,更改了图表实例的提示框的几个默认样式属性。每个提示框将具有灰色背景、黄色 3 像素边框、粉色 16 像素标题、斜体正文和 10 像素箭头,距离数据点 10 像素:

const data = {
   labels: ["One", "Two", "Three", "Four"],
   datasets: [{label:'Dataset 1',… },{label:'Dataset 2',…}, 
   {label:'Dataset 3',…}]
};
new Chart('chart', {type: 'line', data: data,
    options: {
        legend: { display: false },
        tooltips: {
            mode: 'index',
            titleFontSize: 16,
            titleFontColor: 'pink',
            bodyFontStyle: 'italic',
            titleSpacing: 10,
            caretSize: 10,
            caretPadding: 10,
            backgroundColor: 'rgba(10,10,60,.5)',
            borderColor: 'yellow',
            borderWidth: 3,
        },
    }
});

完整代码在Tooltips/tooltip-1.html中。结果如下:

带有修改过的边框颜色和宽度、背景颜色、标题字体大小和颜色、正文字体样式的提示框,

带有刻度箭头大小和填充(从值点到箭头的距离)。代码:Tooltips/tooltip-1.html.

悬停交互

提示框响应悬停事件。下表列出了hover对象的属性,可以通过Chart.defaults.global.hover全局配置或通过options.hover局部配置:

Object Value 描述
intersect truefalse tooltip.intersect相同的行为。提示框可以响应悬停时的不同交叉状态。
mode nearestindexdatasetxy。已弃用的值是label(与index相同),以及single(当intersect: true时,行为类似于nearest)。 tooltip.mode相同的行为。提示框可以响应悬停时的不同模式。
axis xyxy 选择用于在悬停事件期间计算值点到光标距离的参数。默认为x。水平条形图覆盖此为y,以便mode:index可以选中不同的条形。
animationDuration Number 动画持续时间。这会影响任何悬停事件,包括提示框。

悬停对象的配置选项

hover和提示框对象都支持模式(mode)和交叉(intersect)属性。它们很相似,但hover属性还适用于非提示框事件(通过可选的onHover回调配置)。

如果intersect属性为true,则只有在鼠标直接位于条形或饼图块上,或位于值点的一定半径内时(对于线形、散点图和气泡图),事件才会触发。如果intersectfalse,则事件可能在鼠标未位于值点之前触发。

mode属性选择与事件相关的数据值。如果intersectfalse,且modenearest,则将选择最近的点。

当在提示框中使用时,mode属性还确定哪些项目出现在提示框中。如果为nearest,它将显示鼠标所在点最近的价值。该属性还可以有以下值:

  • point,仅显示实际与点相交的项目(典型的内联图表)

  • index,显示相同索引处的所有点(在柱状图或饼图中很常见)

  • dataset,列出数据集中的所有点

还有两种仅适用于笛卡尔刻度的模式:xy,分别选择具有相同xy值的所有点。

编辑 Tooltips/tooltip-3-modes.html 文件并尝试不同的模式。以下截图显示了应用于具有三个数据集的折线图的某些提示信息模式:

提示信息交互模式: (a) 显示单个值点; (b) 显示具有相同索引(或在此情况下为 x 值)的项目;

(c) 显示具有相同 y 值的项目; (d) 显示数据集中的所有项目。代码:Tooltips/tooltip-3-modes.html.

可脚本化属性

提示信息有三个属性接收函数。一个允许你用你自己的自定义 HTML 提示信息替换 Canvas 生成的提示信息。其他两个允许对提示信息项进行排序(当多个项出现在单个提示信息中)和过滤。以下列出了这些属性:

对象 参数 描述
custom (tooltipModel) 用于生成自定义 HTML 提示信息。请参阅本章中 HTML 提示信息的 自定义 HTML 提示信息 部分。
filter (item, data); data.datasets 中的数据集数组;data.labels 中的标签数组;item.xitem.y 包含值点的坐标,item.xLabelitem.yLabel 是每个轴上的标签,item.index 是数据集中项目的索引,item.datasetIndex 是其数据集的索引。 一个返回 truefalse 的函数,在渲染提示信息项之前被调用。如果返回 false,则该项将不会被渲染。
itemSort (item1, item2); 每个参数都是一个具有以下属性的项对象:xyxLabelyLabelindexdataSetIndex 对项目(在包含多个项的提示信息中)进行排序。该函数返回一个数字。如果 item1 < item2,则函数应返回负值;如果 item1 > item2,则应返回正值;如果它们相等,则返回零。

提示信息的可脚本化属性

让我们看看一些示例。在以下代码 (Tooltip/tooltip-4-script-filter.html) 中,过滤函数忽略了所有 y 值大于 20 的项目。此外,使用了 events 键来减少提示信息响应的事件。在此示例中,它们仅在点击时被激活:

const data = {
   labels: ["One", "Two", "Three", "Four"],
   datasets: [{label:'Dataset 1',… },{label:'Dataset 2',…},
   {label:'Dataset 3',…}]
};
new Chart('chart', { type: 'line', data: data,
    options: {
        legend: { display: false },
        tooltips: {
            mode: 'index',
            intersect: false,
            filter: (item, data) => data.datasets[item.datasetIndex]
                                        .data[item.index] < 20
        },
        events: ['click']
    }
});

以下截图显示了点击 index 1 附近的值点时的结果。由于三个点中的一个大于 20,因此它没有显示在提示信息中:

带有模式:仅过滤具有小于 20 的 y 值的项目的提示信息。代码:Tooltip/tooltip-4-script-filter.html.

此示例 (Tooltip/tooltip-5-script-sort.html) 配置了按 y 值的 升序 对项目进行排序,在同一图表中:

new Chart('chart', { type: 'line', data: data,
        options: {
            legend: { display: false },
            tooltips: {
                mode: 'index',
                intersect: false,
                itemSort: (a,b) => b.y - a.y
            },
            events: ['click']
        }
    });

结果如下。请注意,提示信息项是按其 y 值排序的:

带有模式:'index' 的提示信息按其 y 值排序。代码:Tooltip/tooltip-5-script-sort.html.

提示信息回调函数

使用callbacks,您可以根据数据值和其他可访问属性动态生成工具提示中显示的项目文本内容和颜色。Callbackstooltips.callbacks对象的属性,可以在全局(Chart.defaults.global.tooltips.callbacks)或每个图表实例本地配置(options.tooltips.callbacks)。它们在表中按以下方式列出:

对象 参数 描述
beforeTitle, title, afterTitle (item[], data)data.datasets中的数据集数组;data.labels中的标签数组;每个项目元素包含以下属性:xyxLabelyLabelindexdataSetIndex title函数返回工具提示标题的文本。您还可以实现其他函数以包含其上或其下的文本。
beforeBody, body, afterBody body函数返回工具提示主体的文本(包括标签)。您还可以实现其他函数以包含其上或其下的文本。
beforeFooter, footer, afterFooter footer函数返回工具提示脚注的文本。您还可以实现其他函数以包含其前或其后的文本。
beforeLabel, label, afterLabel (item,data)data.datasets中的数据集数组;data.labels中的标签数组;item.xitem.y包含值点的坐标,item.xLabelitem.yLabel是每个轴的标签,item.index是项目在数据集中的索引,item.datasetIndex是其数据集的索引。 label函数返回此标签的文本。您还可以实现其他函数以包含一个或多个标签上或其下的文本。
labelColor (item, chart) 该函数返回单个项目标签文本框的颜色
labelTextColor (item, chart) 该函数返回单个项目标签文本的颜色

用于创建和更改工具提示文本内容的回调函数

以下示例(Tooltips/tooltip-6-callback.html)使用callbacks向标题添加额外文本,在项目标签上下插入分隔符字符,并附加包含所有值点的平均值的footer

new Chart('chart', { type: 'horizontalBar', data: data,
    options: {
        legend: { display: false },
        tooltips: {
            mode: 'index',
            callbacks: {
                footer: (items, data) => 'Average: ' + (data.datasets
                                        .map(d=>d.data[items[0].index])
                                        .reduce((a,b)=>a+b, 0) /     
                                         items.length)
                                        .toFixed(2),
                title: (items, data) => "Stage " + items[0].yLabel,
                beforeBody: () => '============',
                afterBody: () => '------------------',
            }
        },
        events: ['click']
    }
});

结果如下:

使用回调函数创建具有脚注、标题中额外文本以及主体前后分隔符的工具提示。代码:Tooltips/tooltip-6-callback.html.

自定义 HTML 工具提示

Chart.defaults.global.tooltips.custom(或options.tooltips.custom)属性接收一个函数,该函数应构建一个 HTML 工具提示并将其连接到作为参数传递的工具提示模型对象。工具提示模型是一个原生对象,它响应工具提示事件并存储工具提示属性。如果需要,其属性可以在 HTML 工具提示内部复制和重用。

以下示例(Tooltips/tooltip-7-custom.html)展示了如何创建一个包含图像的简单自定义 HTML 提示框。自定义提示框可以使用 HTML 创建,如以下片段所示,或者通过 DOM 编程创建,并且最初应该隐藏(opacity: 0)。当hover事件激活提示框时,模型的透明度会改变,自定义提示框会利用这种状态使自己可见:

<html><head> ...
    <style> 
        #tooltip {
            opacity: 0;
            position: absolute;
            margin: 5px;
        }
    </style>
</head><body>
<canvas id="chart" width="200" height="100"></canvas>
<div id="tooltip"></div>
<script>
    const data = {
        labels: ["jupiter", "saturn", "uranus", "neptune"],
        datasets: [{
           data: [142984,120536,51118,49528],
           backgroundColor: ['#d7191c','#fdae61','#abdda4','#2b83ba'],
        }]
    };
    new Chart('chart', { type: 'bar', data: data,
        options: {
           legend: { display: false },
            title: {
                display: true,
                text: 'Planetary diameters',
                fontSize: 24
            },
            tooltips: {
                mode: 'index',
                intersect: true,
                enabled: false, // turn off canvas tooltips
                custom: function(model) {
                    const tooltip = document.getElementById('tooltip');
                    if(model.opacity === 0) {
                        tooltip.style.opacity = 0;
                        return;                    }
                   if(model.body) {
                        const value = model.body[0].lines[0];
                        tooltip.innerHTML = '<b>'+ value + " km<br/>"
                                            +'<img width="50"  
                                            src="img/>                                            +model.title[0] +'.jpg" 
                                            </img>';
                        const pos =   
                        this._chart.canvas.getBoundingClientRect();
                        tooltip.style.opacity = 1;
                        tooltip.style.left = pos.left + model.caretX +  
                        'px';
                        tooltip.style.top  = -50 + pos.top +  
                        model.caretY + 'px';
                    }
                }
            }
        }
    });
</script>
</body></html>

代码从提示框模型的title中提取标题和文件名,从模型的body中提取值。自定义提示框还使用了模型的坐标来决定其放置的位置。结果如下。如果您将鼠标悬停在x轴的标签或条形上,HTML 提示框将显示在每个条形的上方:

当鼠标悬停在条形或标签上时,会出现自定义 HTML 提示框的条形图。代码:Tooltips/tooltip-7-custom.html.

在饼图中定位提示框要复杂一些。有关如何创建自定义 HTML 提示框的更多示例,请查看官方文档中的示例页面。

高级图例配置

Chart.js 为图例和标签提供了默认的呈现和行为。在前一章中,我们看到了一些如何通过编程onClick事件处理程序回调来更改默认行为的例子。在本节中,我们将看到如何生成单个标签,如果您需要更多的控制,我们将看到如何创建自定义 HTML 图例。

生成标签

标签可以通过generateLabels回调属性生成。它们应该返回一个项目对象(与传递给onClick函数的对象相同),该对象包含以下列出的属性:

对象 描述
text 字符串 标签的文本
datasetIndex 数字 标签的索引

| fillStyle, strokeStyle, lineCap, lineJoin,

lineDash, lineWidth, lineDashOffset | 与相应的 Canvas 命令相同的值 | 图例框的填充和描边属性 |

pointStyle circle, cross, crossRot, dash, line, rect, rectRounded, rectRot, star, triangle 如果legend.labels.usePointStyletrue,则标签将使用与图表相同的点样式。这允许您为图例标签设置不同的点样式。
hidden 布尔值 如果true,则与数据集相关的图表元素将不会渲染

图例标签对象的属性(由onClick接收并由generateLabels返回)

以下代码显示了使用generateLabels回调配置图例标签(Legend/legend-1-gen-labels.html)的配置。彩色框配置为旋转矩形,带有pointStyle。标签的fontSize控制字体和点的大小。每个数据集的边框颜色是每个标签的填充:

options: {
    legend: {
        labels: {
            usePointStyle: true,
            fontSize: 14,
            generateLabels: function(chart) {
                const items = [];
                chart.data.datasets.forEach((dataset, i) => {
                    items.push({
                        text: dataset.label,
                        datasetIndex: i,
                        fillStyle: dataset.borderColor,
                        lineWidth: 0,
                        pointStyle: 'rectRot',
                    });
                });
                return items;
            },
        },  ...
    }
}

结果如下:

为图例生成具有不同符号的标签。代码:Legend/legend-1-gen-labels.html.

HTML 图例

如果你有一个非常复杂的图例或者希望在 Canvas 之外显示图例,与页面中的 HTML 混合,你可以生成自定义 HTML 图例。要创建它们,你需要一个空的<div>块:

<div id="chart-legends"></div>

因此,图例可以附加到页面的主体上。然后,你为Chart.defaults.global.legendCallback属性或options.legendCallback实现一个callback函数,该函数返回图例的 HTML。你可以动态创建内容,并使用从图表复制的属性值应用 CSS 样式。HTML 是通过chart.generateLegend()生成的。

通过示例更容易理解。以下代码从一个 HTML 列表实现了一个简单的 HTML 图例。你可以在Legend/legend-2-html-callback.html中运行完整代码:

const myChart = new Chart("myChart", {
     type: 'line',
     data: {
         labels: ['Day 1','Day 2','Day 3','Day 4','Day 5','Day 6'],
         datasets: datasets,
     },
     options: {
         legendCallback: function(chart) {
             const labels = document.createElement("ul");
             labels.style.display = 'flex';
             labels.style.justifyContent = 'center';

             chart.data.datasets.forEach((dataset, i) => {
                 const item = document.createElement("li");
                 item.style.listStyle = 'none';
                 item.style.display = 'inline';

                 const icon = document.createElement("div");
                 icon.style.width = icon.style.height = '15px';
                 icon.style.display = 'inline-block';
                 icon.style.margin = '0 6px';
                 icon.style.backgroundColor = dataset.borderColor;

                 item.appendChild(icon); // add colored square
                 item.innerHTML += dataset.label; // add label text
                 labels.appendChild(item);
             });
             return labels.outerHTML;
         },
         legend: { display: false, position: 'bottom' }
     }
 });

 const legend = document.getElementById('chart-legends');
 legend.innerHTML = myChart.generateLegend(); // generates HTML

新图例不会替换默认标签。除非你希望显示两个图例,否则你应该使用display: false来隐藏默认图例。

这些 HTML 图例中不包含任何行为。你需要自己使用 JavaScript 事件来实现它们。以下截图显示了之前代码的结果:

图片

使用 HTML 创建的图例。代码:Legend/legend-1-gen-labels.html.

显示多个图表

许多时候,你需要在同一页面上显示多个图表,以展示不同的数据集,或者使用不同的图表类型来展示相同数据的不同视角。你也可能希望在同一轴上绘制多个图表,以便进行比较。另一种可能性是使用 Canvas 在图表之上或之下绘制,以添加上下文或额外数据。所有这些场景在 Chart.js 中都是可能的,但它们需要不同的策略。

在一个页面上渲染多个图表

你可以通过为每个图表绘制一个单独的 Canvas 来在同一页面上渲染多个不同的图表。

以下示例在一个页面上显示了四个共享相同数据的图表。首先,我们需要使用 HTML 和 CSS 设置画布:

<html lang="en">
<head>
     <script src="img/Chart.min.js"></script>
     <style>
         .container {
             width: 98%;
             height: 80vh;
             position: absolute;
         }
         .top {
             height:50%;
             position: relative;
         }
         .col {
             width: 50%;
             position: absolute;
         }
         .col:nth-child(2n-1) {
             left: 50%;
         }
         .footer {
             height: 50%;
             position: relative;
         }
     </style>
 </head>
 <body>
 <div class="container">
     <div class="top" width="400" height="200">
         <div class="col"><canvas id="chart1"></canvas></div>
         <div class="col"><canvas id="chart2"></canvas></div>
     </div>
     <div class="top" width="400" height="200">
         <div class="col"><canvas id="chart3"></canvas></div>
         <div class="col"><canvas id="chart4"></canvas></div>
     </div>
     <div class="footer">
         <form>
           <button type="button" id="changeData">Get Data</button>
         </form>
     </div>
 </div>
 <script> ... </script>
 </body>
 </html>

以下代码展示了 JavaScript 代码。图表最初加载一些静态数据,但每次按下按钮时,数据都会改变,图表也会更新。updateData()函数被创建用来模拟每次按钮按下时加载到每个图表中的新随机数据:

function updateData() {
     charts.forEach(c => {
         let datasets = 3
         if(c.canvas.id == 'chart4') {
             datasets = 1;
         }
         for(let i = 0; i < datasets; i++) {
             for (let j = 0; j < 6; j++) {
                c.config.data.datasets[i].data[j] =  
                Math.ceil(Math.random() * 25);
             }
         }
         c.update();
     });
 }

 Chart.defaults.global.legend.labels.boxWidth = 15;

 const data = [[12, 19, 3, 5, 2, 3],[6, 5, 22, 2, 7, 11],[2, 3, 5, 16,  
 0, 1]],
       labels = ['Day 1','Day 2','Day 3','Day 4','Day 5','Day 6'],
       strokes = 
      ['rgba(54,162,235,1)','rgba(255,99,132,1)','rgba(132,255,99,1)'],
       fills=   
      ['rgba(54,162,235,.2)','rgba(255,99,132,.2)',
      'rgba(132,200,99,.2)'];

 const datasets = [];
 for(let i = 0; i < data.length; i++) {
     datasets.push({
         label: 'Dataset ' + (i+1),
         data: data[i],
         backgroundColor: fills[i],
         borderColor: strokes[i],
     });
 }

 const charts = [];

 charts.push(new Chart("chart1", { type: 'line',
     data: { labels: labels, datasets: datasets }
 }));

 charts.push(new Chart("chart2", { type: 'bar',
     data: { labels: labels, datasets: datasets }
 }));

 charts.push(new Chart("chart3", { type: 'radar',
     data: { labels: labels, datasets: datasets },
     options: {legend: {display: false }}
 }));

 charts.push(new Chart("chart4", { type: 'doughnut',
     data: {
         labels: labels,
         datasets: [datasets[0]].map(d => ({
             data: d.data,
             backgroundColor: ['#d73027','#fc8d59','#fee090',
                               '#e0f3f8','#91bfdb','#4575b4'],
         })),
     },
     options: {legend: {position: 'left'}}
 }));

 document.getElementById("changeData")
         .addEventListener("click", updateData);

你可以看到以下结果。运行Multiple/ multiple-1-canvas.html中的完整代码。按下按钮并观察所有图表同时改变:

图片

在一个页面上显示和更新多个图表。代码:Multiple/multiple-1-canvas.html.

混合图表

混合图表是不同类型的图表,它们共享相同的轴。一个典型的例子是将柱状图与一个或多个折线图叠加。在 Chart.js 中,这可以通过在数据集的一个或多个中添加不同的type属性来实现。

在以下示例(Multiple/ multiple-2-mixed.html)中,使用条形图显示一组值,并使用折线图显示累积平均值:

const values = [12, 33, 42, 67, 90, 56, 51, 78, 95, 101, 120, 140];
const averages = [];
for(let i = 0; i < values.length; i++) {
     averages[i] = values.slice(0,i+1).reduce((a,b)=>a+b,0)/(i+1);
 }

 new Chart("myChart", {
     type: 'bar',
     data: {
         labels: ['Jan','Feb','Mar','Apr','May','Jun','Jul',
                  'Aug','Sep','Oct','Nov','Dec'],
         datasets: [{
             type: 'line',
             label: 'Line dataset (average)',
             data: averages,
             borderColor: 'red',
             fill: false
         },{
             label: 'Bar dataset (totals)',
             data: values,
             borderColor: 'blue',
             backgroundColor: 'rgba(0,0,120,.6)'
         }]
     }
 })

由于bar是默认类型,因此不需要type属性。每种类型也可能有额外的数据集。

结果如下:

图片

混合条形图/折线图。代码:Multiple/multiple-2-mixed.html.

在画布上叠加

在图表上绘制文本和图形的一种方法是在图表完全加载后在其相同的画布上绘制。您可以通过将代码实现为分配给animation.onComplete属性的函数来完成此操作。您还可以编写一个简单的插件。另一种在图表上方或下方绘制的方法是在另一个画布上绘制,并将其定位在您的图表画布的正好上方或下方。如果您不会调整页面大小,这很容易做到。如果您进行任何调整大小,您将不得不编写额外的脚本以缩放画布内容,以保持与图表(在这种情况下,插件将是一个更好的解决方案)同步。

例如,让我们使用我们在第二章中加载和渲染的GeoJSON世界地图,技术基础,并将其放置在第四章中创建的气泡图下,创建图表的城市人口。由于地图使用简单的圆柱投影,我们只需使它们大小相同,并使用 CSS 绝对定位将一个堆叠在另一个之上:

<html lang="en">
 <head>
     <script src="img/canvasmap.js" ></script>
     <script src="img/Chart.min.js"></script>
     <script src="img/papaparse.min.js"></script>
     <style>
         canvas {
             position: absolute;
             top:  0;
             left: 0;
         }
     </style>
 </head>
 <body>

<canvas id="map" width="1000" height="500"></canvas>
<canvas id="my-bubble-chart" width="1000" height="500"></canvas>
<script>...</script>
</body></html>

绘图也必须从相同的位置开始,并使用相同的比例。代码使用了JavaScript/canvasmap.js中的四个函数:一个简单的脚本,用于从 GeoJSON 数据绘制地图:

  • map.setCanvas(canvas):接收将要绘制地图的背景画布

  • map.drawMap(geodata):接收一个 GeoJSON 特征的数组并绘制地图

  • map.scaleX(longitude)map.scaleY(latitude):将经纬度转换为像素坐标

以下代码获取地图的canvas上下文,并设置其填充和描边样式,加载并解析包含世界地图形状的 GeoJSON 文件,以及包含城市名称、人口、纬度和经度的 CSV 文件。然后调用函数绘制地图和图表:

const mapCanvas = document.getElementById("map");
const mapContext = mapCanvas.getContext("2d");

 // Map ocean background
 mapContext.fillStyle = 'rgb(200,200,255)';
 mapContext.fillRect(0, 0, mapCanvas.width, mapCanvas.height);

 // countries border and background
 mapContext.lineWidth = .25;
 mapContext.strokeStyle = 'white';
 mapContext.fillStyle = 'rgb(50,50,160';

 // setup map canvas
 map.setCanvas(mapCanvas); // Function from JavaScript/canvasmap.js

 // load files
 const files = ['../Data/world.geojson', '../Data/cities15000.csv'];
 const promises = files.map(file => fetch(file).then(resp =>   
 resp.text()));
 Promise.all(promises).then(results => {

     // Draw the map
     const object = JSON.parse(results[0]);
     map.drawMap(object.features); // function from  
     JavaScript/canvasmap.js

     // Draw the chart
     const data = Papa.parse(results[1], {header: true});
     drawChart(data.data);  // function described below
 });

每个气泡的半径将与人口数量成一定比例。此函数将返回一个适合在地图上显示的值:

function scaleR(value) {
     const r = Math.floor(value / 100000);
     return r != 0 ? r/10 : .25;
 }

drawChart()函数使用解析的 CSV 数据集生成一个包含位置对象的数组,每个对象包含名称和气泡图所需的属性:r半径和xy坐标。生成的locations数组用作气泡图的dataset

function drawChart(datasets) {
     const locations = [];
     datasets.forEach(city => {
         const obj = {
             x: map.scaleX(+city.longitude), // From  
             JavaScript/canvasmap.js
             y: map.scaleY(-city.latitude),  // From 
             JavaScript/canvasmap.js
             r: scaleR(city.population),
             name: city.asciiname
         };
         locations.push(obj);
     });

     const dataObj = {
         datasets: [
             {   data: locations,
                 backgroundColor: function(context) {...}
             }
         ]
     }

options 配置对象必须配置刻度,以确保没有边距。设置刻度的 minmax 属性,移除图例,并设置 responsive:false 将保证这一点。工具提示也被配置为显示名称和人口(这里没有显示,但可以在 Multiple/multiple-3-overlay.html 中看到完整的代码):

    const chartObj = {
         type: "bubble",
         data: dataObj,
         options: {
             scales: {
                 xAxes: [{ display: false,
                           ticks: {
                             min: map.scaleX(-180), // match map size  
                             with
                             max: map.scaleX(180)   // canvas size
                         }
                     }
                 ],
                 yAxes: [{ display: false,
                           ticks: {
                             min: map.scaleY(-90), // match map size 
                             with
                             max: map.scaleY(90)   // canvas size
                         }
                     }
                 ]
             },
             tooltips: {...}, // see full code
             animation: { duration: 0 },
             responsive: false,
             legend: { display: false }
         }
     };
     new Chart("my-bubble-chart", chartObj);
 }

最终结果是如下。图表是交互式的;你可以悬停在大型城市上以获取详细信息:

两个堆叠的 HTML Canvas:一个绘制 SVG GeoJSON 地图,另一个使用 Chart.js 绘制气泡图。

代码:Multiple/multiple-3-overlay.html.

由于在这个示例中使用了非常大的文件,加载图表需要一些时间,并且在某些系统上工具提示可能运行得有点慢。一种快速优化方法是,在加载之前减少数据文件的大小。也可以过滤并仅使用大城市,用 Canvas 分别绘制小城市。

扩展 Chart.js

有几种方法可以扩展 Chart.js。可以使用原型方法、回调和事件处理器与渲染过程交互;可以创建插件,它们有自己的生命周期,并且更容易在其他图表中重用;还可以从现有图表或甚至创建新的图表和刻度扩展 Chart.js。

原型方法

原型方法在渲染和更新过程中自动调用。如果需要干扰渲染过程,也可以直接调用它们。它们列在以下表中:

方法 描述
destroy() 销毁图表实例。如果希望重用画布或完全删除图表,可以使用此方法。
reset() 将图表恢复到其初始状态(布局之后和初始动画之前)。可以使用 render()update() 触发新的动画。
stop() 停止动画循环。这通常在 onProgress 回调中调用。调用 render()update() 将继续动画。
clear() 清除图表画布(在图表完成渲染后生效)。可以调用 render()update() 重新绘制它。
resize() 调整图表大小。每当画布大小调整时自动调用。
update(config) 更新图表。在数据集有任何更改后应调用此方法。可以包含一个具有以下属性的配置对象:duration(数字)用于控制重绘动画的持续时间,lazy(布尔值)用于决定动画是否可以被其他动画中断,以及 easing(字符串),用于选择缓动函数。
render(config) 重新绘制所有图表元素,但不更新新数据对应的图表元素。
toBase64Image() 将图表生成一个新的 base64 编码的 PNG 图像。它可以在 HTML 页面上显示,或转换为 blob 以供下载。
generateLegend() 当调用时,返回 options.legendCallback 属性的内容(一个 HTML 图例)。
getElementAtEvent(e) 在事件处理程序中使用,以获取事件中的元素。
getElementsAtEvent(e) 在事件处理程序中使用,以获取在事件中具有相同数据索引的所有元素。
getDatasetAtEvent(e) 在事件处理程序中使用,以获取属于数据集的元素数组。
getDatasetMeta(index) 返回与索引对应的数据集的元数据。

Chart.js 原型方法

其中一些方法用于触发插件中生命周期回调的执行。许多方法已经被自动调用,并且可能不是在动画的所有阶段都有效,因为其他阶段可能会调用取消所需效果的方法。

在本章中,我们看到了 generateLabels() 的一个示例,在前一章中,我们使用了 update()。事件方法在事件处理程序中很常见,这些处理程序接收一个 JavaScript 事件。

toBase64Image() 方法生成一个 Base64 图片字符串。在 animation.onComplete 或在任何仅当图表完全绘制时调用的 callback 函数中调用它(否则可能会生成部分绘制的或空白图片)。它返回一个字符串,可以分配给 HTML 图像的 src 属性,以便在 HTML 页面上进行渲染:

<image id="image"></image>
…
<script>
new Chart("chart", { type: 'line', data: {…},
     options: {
         animation: {
             onComplete: function () {
                 let image = document.getElementById('image');
                 image.src = this.toBase64Image();
        }
    }
});
</script>

您还可以使用它通过 blob 函数创建用于下载的图片。使用从 www.npmjs.com/package/b64-to-blob 可用的 b64-to-blob 函数,或者通过在您的页面上添加以下行通过 CDN 使用:

<script src="img/b64-to-blob"></script>

在您想要下载链接的位置添加以下标签:

<a id=’link’ download=’chart.png’></a>

然后,将此代码放置在 animation.onComplete 函数中:

const link = document.getElementById('link');
const blob = b64toBlob(image.src.split(',')[1], 'image/png');
link.href = URL.createObjectURL(blob);

在图表加载后,它将创建一个链接,点击该链接将下载图表的 PNG 图片。完整的代码位于 Extensions/ext-1-prototype.html

创建插件

插件是扩展 Chart.js 的最高效方式。插件可以在图表渲染周期的不同阶段之前和之后插入代码。在每个阶段,它可以访问图表对象并读取可配置的选项。这可以用来更改图表的几乎所有属性或行为。

插件被设计成可重用的。在前几章中,我们使用了几个流行的插件以不同的方式扩展了 Chart.js。它们非常适合封装复杂性,但简单的插件也非常有用。

在最后一个例子中,我们为图表的 PNG 版本创建了一个下载链接。如果你尝试过,你可能已经注意到图像有一个透明的背景。如果你的背景是白色的,这没问题,但如果不是,图表可能难以阅读。解决这个问题的天真方法是用 CSS 或填充命令将画布涂成白色。但是,这不会起作用,因为 Chart.js 在其渲染周期中会重新绘制画布。你还需要处理任何动画、调整大小、更新和其他可能在你更改颜色后重置背景的事件。这是一个插件用例。使用插件,你可以在渲染周期中插入代码,在初始化画布之后、绘制图表之前绘制背景。

Chart.js 的渲染生命周期如下所示。当图表首次加载时,它执行 initupdaterender 步骤。每次页面调整大小,都会执行 updaterender,在 events 上,执行 render 步骤:

Chart.js 生命周期。每个阶段都可以被插件回调拦截。

根据你的插件,你可能需要拦截这些步骤中的一个或多个。以下表格列出了插件可用的回调函数。每个 callback 函数至少包含两个参数:对图表实例的引用和一个 options 对象(在 options.plugins 下的插件 ID 键配置)。一些回调可能还有其他参数:

方法 参数 描述
beforeInit afterInit (chart, options) 在调用新的 Chart() 之前和之后调用
beforeUpdate afterUpdate (chart, options) update 阶段之前和之后调用
beforeLayout afterLayout (chart, options) layout 阶段之前和之后调用
beforeDatasetsUpdate afterDatasetsUpdate (chart, options) 在更新所有数据集之前和之后调用
beforeDatasetUpdate afterDatasetUpdate (chart, dataset, options) 在更新每个数据集之前和之后调用
beforeRender afterRender (chart, options) render 阶段之前和之后调用
beforeDraw afterDraw (chart, easing, options) draw 阶段之前和之后调用
beforeDatasetsDraw afterDatasetsDraw (chart, easing, options) 在绘制所有数据集之前和之后调用
beforeDatasetDraw afterDatasetDraw (chart, dataset, options) 在绘制每个数据集之前和之后调用
beforeEvent afterEvent (chart, event, options) events 之前和之后调用
resize (chart, dimensions, options) 在调整大小后调用
destroy (chart, options) 在调用 chart.destroy() 之后调用

可以在插件中使用的生命周期回调

要查看这些方法的演示,运行Extensions/ext-2-plugin-lifecycle.html文件。它会在渲染和销毁具有三个插件的图表时记录每个生命周期事件。

插件是一个简单的对象。除非您计划在*options*对象中配置插件,否则不需要id属性。您可以仅包含所需的回调属性。以下代码将创建一个简单的可配置插件,该插件将在图表前绘制一个蓝色方块,并在坐标轴前绘制一个红色方块,但位于条形图后面(Extensions/ext-3-simple-plugin.html):

const plugin = {
     id: 'p1',
     afterRender: function(chart, options) {
         chart.ctx.fillStyle = 'blue';
         chart.ctx.fillRect(60,60,100,100);
     },
     beforeDatasetsDraw: function(chart, percent, options) {
         chart.ctx.fillStyle = 'red';
         chart.ctx.fillRect(200,60,100,100);
     },
 };

此效果在此处显示:

在图表中使用简单的插件绘制了蓝色和红色方块。代码:Extensions/ext-3-simple-plugin.html.

如果您在本地编写插件并且有多个图表,您可以使用Chart()构造函数中的*plugins*键包含要添加到每个图表的插件列表。它接受一个数组:

new Chart("chart", {
     type: 'bar',
     data: {…},
     options: {…},
     plugins: [plugin1]
 });

应尽可能使插件可重用。可重用插件通常在单独的.js文件中创建,并自动对所有图表可用。在这种情况下,它们应使用以下方式全局注册:

Chart.plugins.register(plugin);

让我们为最后一个示例创建一个插件,以便图片和图表都有不透明的背景。插件应具有可配置的选项。如果您可以为每个图表配置背景颜色,那么重用此插件的可能性更大。我们还可以添加绘制背景图片的可能性。该插件将存储在单独的 JavaScript 文件JavaScript/chartback.js中,该文件创建插件对象并将其全局注册。id是必要的,这样图表就可以识别插件并配置其选项。

由于每次图表大小调整或更新时都需要重新绘制图片,因此最佳绘制位置是在beforeDraw回调中。此代码还将图片放置在坐标轴后面:

const bgPlugin = { id: 'chartback',
    beforeDraw: function(chart, steps, options) {
       let ctx = chart.ctx;
        if(options.backgroundColor) {
            ctx.fillStyle = options.backgroundColor;
            ctx.fillRect(0, 0, chart.width, chart.height);
        }
        if(options.backgroundImage) {
            let image = new Image(chart.width, chart.height);
            image.src = options.backgroundImage;
            ctx.drawImage(image, 0,0,chart.width, chart.height);
        }
    }
}
Chart.plugins.register(bgPlugin);

要使用此插件,将其导入到将创建图表的 HTML 文件中:

<script src="img/chartback.js"></script>

此插件的可配置选项可以在options.plugins.chartback键(chartback是插件的 ID)中设置。此代码位于Extensions/ext-4-chartback.html

new Chart("chart", { type: 'bar', data: {…},
    options: {
        animation: {…},
        plugins: {
            chartback: {
                backgroundColor: 'white',
                backgroundImage: '../Images/mars.jpg'
            }
        }
    },
});

图表将带有背景图片绘制。如果您不想显示图片,只需设置backgroundColor,就可以得到一个具有不透明背景的图表。以下截图显示了包含图表和由图片查看器应用程序加载的.png文件的网页:

使用一个将背景图片放置在图表后面的插件。代码:Extensions/ext-4-chartback.html.

Chart.js 扩展

除了插件,Chart.js 还包括一个高级扩展 API,你可以使用它来扩展图表和坐标轴。使用此 API,你可以通过实现提供的接口从现有的图表类型派生,或者创建全新的图表类型。这个主题超出了本书的范围,但你可以在官方文档中尝试提供的示例,或者可能使用一些已经广泛可用的许多流行扩展。官方文档中列出了其中的一些。

摘要

在本章中,我们探讨了几个你可能很少需要但能让你对图表的外观和感觉有更多控制的 Chart.js 高级主题,允许高度定制,并有可能将其与标准 Web 技术和框架集成。

我们学习了如何使用原生 Canvas 选项和自定义 HTML 扩展来配置工具提示和图例的展示和行为,以及如何使用 Canvas 创建包含多个图表、混合图表和与其他图形叠加的图表页面。我们还使用 Chart.js 的编程 API 编写了扩展,以生成图表的 PNG 版本,并创建了一个简单的插件,该插件可以向图表添加背景图片。

参考文献

书籍和网站:

数据来源:

posted @ 2025-09-26 22:07  绝不原创的飞龙  阅读(86)  评论(0)    收藏  举报