React-和-D3-js-集成教程-一-

227 阅读35分钟

React 和 D3.js 集成教程(一)

原文:Integrating D3.js with React

协议:CC BY-NC-SA 4.0

一、设置我们的技术栈

集成交互式数据可视化(又名数据即)组件可以帮助你更好地讲述你的故事。React 已经设置为能够动画可缩放矢量图形(SVG)、HTML 和 Canvas 这不是什么新鲜事。多年来,我们已经有能力用 HTML 和纯 JavaScript 制作 SVG、Canvas 和 HTML 动画。React 具有 HTML、CSS 和 JavaScript 功能,可以很好地与其他库配合使用,帮助创建图表和动画视图。

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

React 相对于其他 web 平台的最大优势是它使用了虚拟 DOM (VDOM ),从而提高了性能。我们可以利用 React 所提供的以及其他第三方库,如 D3 库和数据管理,来处理我们的数据,不仅构建引人注目的图表组件,还可以提高性能。同时。有时,我们希望取得控制权,让我们的组件控制元素,而不是做出 React。

添加其他库和技术,比如反冲、Material-UI、TypeScript、单元测试、时髦的层叠样式表(SCSS)等等,将需要更多的知识,但这是我们所得到的小小代价。

在这本书里,我会给你一些工具来学习如何用 React 创建数据可视化,我们会从大量的 D3 库以及 React 世界里的其他标准库那里得到帮助。

这第一章作为一个介绍。我们将回顾我们将在本书中使用的工具来创建动画和图表,以及设置我们的第一个“Hello World”D3/React/TypeScript 项目。此外,我们将看看我们能做些什么来确保质量,研究单元测试、林挺和格式化。

我们开始吧。

React

React(也称为 ReactJS)是一个 JavaScript 库,由脸书( https://github.com/facebook/react )开发,用于创建 Web 用户界面。

React 是由乔丹·沃克发明的,他当时正在做脸书的广告。它与其他 web 框架和库如 jQuery、Angular、Vue.js、Svelte 等竞争。

在 2017 年 9 月发布的上一版本 React 16.x 中,React 团队在消除 bug 的同时,增加了更多的工具和开发支持。React 的最新版本(撰写本文时)是 17,发布于 2020 年 10 月。

Note

React 17 是一个“垫脚石”版本,该版本主要专注于使 React 更容易升级到未来版本,以及增加与浏览器的更好兼容性。React 团队的支持表明该库势头强劲,不会很快消失。

为什么要 React?

你知道吗?

  • React 是开发人员的最爱。事实上,根据一项 Stack Overflow 调查( https://insights.stackoverflow.com/survey/2020 ),React 是最受欢迎的 web 框架并且已经连续两年了。

  • 对 React 开发人员的需求激增;据 Indeed.com(https://www.indeed.com/q-React-jobs.html)统计,React 开放的开发者岗位接近 56000 个。

  • React 库很轻(大约 100KB)并且很快。

  • React 很容易上手使用。

对优势和局限性做出 React

正如我提到的,当 React 与 jQuery、Angular 和 Vue.js 等其他 web 框架相比时,最大的优势是 React 使用了 VDOM,可以提高性能。这里还有几个优点:

  • React 可以用作单页应用(SPA ),如 Create-React-App (CRA ),或者用于服务器端渲染(SSR ),如 Gatsby.js 和 Next.js

  • React 可以遵循单向数据流以及数据绑定。

  • React 的 JSX 产生了更好的代码可读性。

  • React 可以很容易地与其他框架集成。

React 有一些限制,如下所示:

  • React 本身只是一个 UI 库,而不是 Angular 那样的成熟框架。

  • 开发人员可以决定添加哪些库以及遵循哪些最佳实践。

  • 与其他工具相比,React 的学习曲线更加陡峭。

React 模板启动项目

在创建 React 应用时,有许多选项可供选择。您可以自己编写代码,然后添加库来帮助您打包代码,为生产做好准备(工具链),并完成编写代码时的其他常见标准任务。

Note

React 工具链是一套编程工具,用于为我们的最终开发/部署产品执行复杂的开发任务。

开始的另一个选择是使用许多 React starter 模板项目,这些项目已经负责搭建和配置,并包括帮助您快速完成工作的库。创建 React app 最流行的模板是 Create-React-App(https://github.com/facebook/create-react-app);该项目由脸书创建,在 GitHub 上有 85,000 颗星星。CRA 是基于一个单页面的应用,所以没有页面刷新,这种体验就像你在一个移动应用里面。这些页面应该在客户端呈现。这是中小型项目的理想选择。

另一个选项是 SSR,它在服务器上呈现页面,因此客户端(浏览器)无需做任何工作就可以显示应用。SSR 适用于某些用例,在这些用例中,如果渲染发生在客户端,用户体验会很慢。

CRA 不支持现成的 SSR。有一些方法可以配置 CRA 并使其与 SSR 一起工作,但是对于一些开发人员来说,这可能太复杂了,并且需要您自己维护配置,所以可能不值得花费精力。

如果您正在构建需要 SSR 的东西,最好是使用已经配置好的带有 SSR 的不同 React 库,如 Next.js framework、Razzle 或 Gatsby(在构建时将预呈现的网站转换为 HTML)。

如果你更喜欢带有 React 的 SSR,可以看看 Next.js、Razzle 或 Gatsby。

也就是说,使用 CRA,你可以进行预渲染,这是最接近 SSR 的方法,在本书后面的章节中,当我们优化 React 应用时,你会看到这一点。

在本书的例子中,我们将使用 CRA;然而,我们将要构建的组件是松散耦合的,可以很容易地导入到任何 React 项目中,几乎不需要任何努力。

Tip

我们将在本书中使用 CRA,项目将很容易理解。但是,可以随意使用任何 React 模板启动项目,甚至从头开始创建自己的 React 项目,并处理自己的工具链。在第十章,我将向你展示如何使用 SSR 和 Next.js 来设置你的 React 项目。

先决条件

我们将要安装的库被提交给 NPM ( https://www.npmjs.com/ )。需要 Node.js 来获得 NPM,使用 NPM 从 NPM 仓库下载包。

NPM 和 Node.js 携手并进。NPM 是 JavaScript 包管理器,也是 JavaScript Node.js 环境的默认包管理器。

在 Mac/PC 上安装节点和 NPM

如果你没有安装 Node.js,你需要安装它。Node.js 至少需要 8.16.0 或 10.16.0 版本。我们需要那个版本的原因是我们需要使用 NPX,这是 2017 年推出的 NPM 任务运行器,用于设置 CRA。

通过检查版本来确保您拥有它,如下所示:

$ node -v

如果没有安装,你可以从这里为 Mac 和 PC 安装(图 1-1 ):

img/510438_1_En_1_Fig1_HTML.jpg

图 1-1

在 Mac 上下载 Node.js

https://nodejs.org/en/

安装程序可以识别你的平台,所以如果你在 PC 上,步骤是一样的。

一旦你下载了安装程序,运行它;一旦完成,在终端/DOS 中运行node命令。

$ node -v

该命令将输出 Node.js 版本号。

下载库:纱线或 NPM

要从 NPM 资源库下载包,我们有两个选项:Yarn 或 NPM。NPM 附带 Node.js,无需安装即可使用。然而,在本书中,我们大多会使用另一个库:Yarn。我们将尽可能多地使用纱线来下载软件包,而不是 NPM。

我们在这本书里用纱代替 NPM 的原因是纱比 NPM 快。Yarn 缓存已安装的包并同时安装包。我们还安装了 NPM,因为它是 Node.js 附带的

在 Mac/PC 上安装 Yarn

要在 Mac 上安装 Yarn,一个好的选择是在终端中安装brew

$ brew install yarn

就像 Node.js 一样,用-v标志运行 Yarn 输发布本号。

$ yarn -v

在 PC 上,您可以从以下位置下载 MSI 下载文件:

https://classic.yarnpkg.com/latest.msi

您可以在此找到更多安装选项:

https://classic.yarnpkg.com/en/docs/install/#mac-stable

创建-React-应用 MHL 模板项目

配备了 Node.js 以及 NPM 和 Yarn,我们就可以开始了。我们可以在 https://github.com/EliEladElrom/cra-template-must-have-libraries 使用我为您创建的 CRA 必备图书馆(MHL)模板项目。

CRA 坚持己见,包括诸如 Jest、service workers 和 ES6 等库。MHL 模板项目甚至更加固执己见,包括以下库:

  • 打字检查器:打字稿

  • 预处理器:萨斯/SCSS

  • 状态管理 : Redux 工具包/反冲

  • CSS 框架:素材-UI

  • CSS-in-JS 模块:样式化组件

  • 路由:React 路由

  • 单元测试 : Jest 和 Enzyme + Sinon

  • E2E 测试:笑话和木偶师

  • 文件夹结构

  • 生成模板

  • 埃斯林特和更漂亮

  • 其他有用的库 : Lodash,Moment,Classnames,Serve,react-snap,React-Helmet,Analyzer Bundle

如果您想了解这些库是如何安装的,您可以创建自己的模板或修改现有的模板。那超出了本书的范围;但是,您可以阅读本文,了解每个库的完整分步安装:

https://medium.com/react-courses/setting-up-professional-react-project-with-must-have-reactjs-libraries-2020-9358edf9acb3

或者你可以参加我在 Udemy 上的课程:

https://www.udemy.com/course/getting-started-react17-with-must-have-libraries/

让我们首先用一个命令创建我们的react-d3-hello-world“Hello World”项目,如下所示:

$ yarn create react-app react-d3-hello-world --template must-have-libraries

或者我们可以使用npx,如下图所示:

$ npx create-react-app react-d3-hello-world --template must-have-libraries

一旦库和所有依赖项的安装完成下载,您就可以通过启动本地服务器来运行项目。

将目录切换到react-d3-hello-world项目,在终端运行start命令(见图 1-2 )。

img/510438_1_En_1_Fig2_HTML.jpg

图 1-2

CRA 编译成功

$ cd react-d3-hello-world
$ yarn start

您可以在package.json文件中看到这个运行命令。该命令指向react-scripts库,并在默认端口 3000(您可以更改)上的本地服务器上启动项目。

现在导航到本地主机并查看项目,如图 1-3 所示。

img/510438_1_En_1_Fig3_HTML.jpg

图 1-3

运行 CRA 启动项目的本地服务器

类型检查器:类型脚本

在编写 React 代码时,有两个选项可供选择:可以使用 JavaScript (JS)或 TypeScript (TS)编写代码。TypeScript 是 transpiler,这意味着 ES6 不理解 TS,但 TS 会被编译成标准的 JS,这可以用 Babel 来完成。

CRA·MHL 项目已经设置了 TS 作为开箱即用的类型检查器,因此您无需做任何事情。然而,我想扩展一下为什么我选择 TS 而不是 JS。

为什么应该将 TypeScript 集成到 React 项目中?

以下是一些有趣的事实:

  • 您知道 TypeScript 是由微软开发和维护的开源编程语言吗?

  • 根据 Stack Overflow 2020 年的调查,TypeScript 编程语言是第二受欢迎的语言,去年甚至超过了 Python!

为什么 TypeScript 这么受欢迎?

TS vs. JS,有什么大不了的?

顾名思义,TS 就是设置“类型”TS 比 JS 更容易调试和测试,并通过描述预期的内容来防止潜在的问题(当我们在本书后面测试我们的组件时,您会看到这一点)。使用 TS,一种成熟的面向对象编程(OOP)语言和模块将开发带到了更专业的水平,并提高了我们的代码质量。

如果我们做一个 TS 对 JS 的快速比较:

  • TypeScript 是一个 OOPJavaScript 是一种脚本语言。

  • TypeScript 使用遵循 ECMAScript 规范的静态类型。

  • TypeScript 支持模块。

类型系统将一个类型与每个值相关联—通过检查这些值的流程,它确保没有类型错误。

静态类型意味着在运行之前检查类型(允许您在运行之前跟踪 bug)。

JS 只包括以下八种动态(运行时)类型:BigInt、Boolean、Integers、Null、Number、Strings、Symbol、Object(对象、函数和数组)和 Undefined。

Note

所有这些类型都被称为原始类型,除了 Object,它被称为非原始类型。TS 通过设置编译器对源代码进行类型检查,将静态类型转换为动态代码,从而为 JavaScript 添加静态类型。

React 和 TypeScript 配合得很好,因为 TypeScript 使用 OOP 最佳实践提高了应用的代码质量,所以值得学习。

TS 的最新版本是版本 4 公共迭代。要在 TS 中玩编码,可以在 https://www.typescriptlang.org/play/ 的 TS 游乐场运行 TS 代码(见图 1-4 )。

img/510438_1_En_1_Fig4_HTML.jpg

图 1-4

TS 游乐场

TS 游乐场网站有大量的例子,可以帮助您更好地了解 TS。我建议探究这些例子。

请注意,该示例使用了“strict”,在 TS Config 菜单项中,您可以设置编译器选项。不同的编译器选项在 https://www.typescriptlang.org/docs/handbook/compiler-options.html 中解释。

这可能会使你的代码在编译时出现错误和警告,但这是值得的,因为它将帮助你避免以后编译器无法识别类型和你的应用在运行时中断的问题。

Tip

我们宁愿我们的应用在编译时中断,而不是在运行时。

我提到 TS 是 OOP 语言,遵循 ECMAScript 规范;然而,规范是动态的,经常变化,所以您可以指定 ECMAScript (ES)目标。参见图 1-5 。

img/510438_1_En_1_Fig5_HTML.jpg

图 1-5

指定 TS 操场中的 ECMAScript 目标

从 TS 开始的一个很好的地方是通过查看不同的可用类型来理解它的功能。如果您刚刚开始使用 TS,解释类型超出了本书的范围,但是我欢迎您查看下面的文章,其中还包括一个带有大量示例的备忘单:

https://medium.com/react-courses/instant-write-reactjs-typescript-components-complete-beginners-guide-with-a-cheatsheet-e32a76022a44

D3

D3(又名 D3js 或 D3.js)代表“数据驱动文档”,它使您能够创建整洁的数据驱动文档( https://github.com/d3/d3 )。这是一个图表库,有助于将数据变得生动。它是由 Mike Bostock 在纽约时报创建的,用于创建交互式网络可视化。这是基于他在斯坦福可视化小组攻读博士期间的工作。

  • D3 包括一个全面的库,有将近 170 个例子。 https://observablehq.com/@d3/gallery

  • D3 利用了这些基础技术:JavaScript、HTML、CSS3、Canvas 以及最后但同样重要的 SVG。

D3.js 是一个基于数据操作文档的 JavaScript 库。D3 使用 HTML、SVG 和 CSS 帮助你将数据变得生动。D3 对 web 标准的重视让您拥有现代浏览器的全部功能,而无需将自己束缚在一个专有的框架中,结合了强大的可视化组件和数据驱动的 DOM 操作方法。

https://d3js.org/

D3 是用 JavaScript 编写的,重点是将数据附加到文档对象模型(DOM)元素上。

典型的纯 D3 的过程可以分为三个部分。

  • Attach :将数据附加到 DOM 元素上。

  • 显示:使用 CSS、HTML 和/或 SVG 来显示数据。

  • Interactive :使用 D3 数据驱动的转换和转换使数据具有交互性。

撰写本文时的最新版本是 v6。阅读 changelog ( https://github.com/d3/d3/blob/master/CHANGES.md )来看看版本 6 有什么新变化。

D3 有一个陡峭的学习曲线,添加 React 和 TypeScript 使曲线更加陡峭。

其实 D3 有 30 多个模块,1000 个方法!要深入了解 D3,D3 团队提供的免费资源很少。

D3 版本> 4

正如我提到的,D3 已经是第六次迭代了。在 D3 版本 4 及以上,最大的变化是 D3 是模块化的。由于这种模块化,您可以只导入需要的东西,而不是带来整个厨房水槽。这里有一个例子:

$yarn add d3-axis d3-interpolate d3-scale d3-selection

请记住,对于 TS,我们还需要引入类型(yarn @types/module-name)。

Tip

当你在网上看例子时,比如在 https://observablehq.com/@d3/gallery 的例子,检查例子中使用的 D3 版本是很重要的。D3 v4 和更高版本已经经历了重大的变化,升级需要对 D3 库有很好的了解。

其他数据即库

除了 D3,还有许多其他基于 D3 构建的 React 库,包括现成的组件。以下是一些比较受欢迎的:

在第九章中,我将向你展示如何实现这些流行的库,同时回顾这些库来帮助你选择一个。

许多人会认为我们最不需要的就是另一个图表组件库,他们是对的。如果你需要现成的图表,有很多可供选择。

也就是说,使用图表库创建真正创新和高性能的可视化可能是一个挑战,您可能会发现自己需要使用 D3 或派生现有的图表库并进行更改。

除了 D3 和 React 库,还有其他高级库可以使用,比如 Vega ( https://vega.github.io/vega-lite/ )。

此外,还有商业智能(BI)工具,如 Tableau 或 PowerBI,您可以使用它们来分析数据,并将可视化集成到 React 项目中。

请记住,除了我提到的顶级库之外,GitHub 还充斥着制作终极图表库的失败尝试。您将很快淹没在选项、标志和props中,以满足所有的用例。

D3 是最终的“图表库”如果你想做一个定制化的数据可视化,那就花时间学习 D3 吧。

也就是说,有一些特定的项目需要更快的发布或者更多的定制,比如概念验证(POC)项目。在这些情况下,知道那里有什么以及如何集成或派生它将会派上用场,所以我将在第九章中介绍一些这样的库供你参考。

ReactTransitionGroup 附加组件

除了 D3,ReactCSSTransitionGroup是 React 库的一个例子,它提供了基于ReactTransitionGroup的高级 API。当 React 组件进入或离开 DOM 时,这是一种执行 CSS 过渡和动画的简单方法(该库的灵感来自 Angular 的ng-animate)。

你可以把这个库和其他的库一起使用,比如 D3;你可以在这里了解更多: https://reactjs.org/docs/animation.html

React v17 + D3 +类型脚本

我们已经介绍了名为react-d3-hello-world的起始项目,并查看了 TypeScript 和 D3,所以我们现在可以将它们放在一起,创建我们的第一个“Hello World”D3 项目。

首先,安装 D3 和所有的 D3 类型(对于 TS)。

$ yarn add d3 @types/d3

将功能组件与 D3 React

接下来,让我们创建一个简单的组件;我们称它为HelloD3 .,因为我们将使用一个我用generate-react-cli库( https://github.com/arminbro/generate-react-cli )创建的模板来移动文件和创建文件夹结构。

npx generate-react-cli component HelloD3 --type=d3

这里可以看到设置:generate-react-cli.json。导航到src/components,可以看到自动为你生成了三个文件(见图 1-6 )。

img/510438_1_En_1_Fig6_HTML.jpg

图 1-6

HelloD3 组件

  • HelloD3.scss

  • HelloD3.test.tsx

  • HelloD3.tsx

App.tsx

打开我们的 app 入口点App.tsx,添加我们创建的HelloD3组件。

import React from 'react'
import './App.scss'
import HelloD3 from './components/HelloD3/HelloD3'

function App() {
 return (
  <div className="App">
   <header className="App-header">
    <HelloD3 />
   </header>
  </div>
 )
}

export default App

现在,我们再来看看 localhost】(见图 1-7 )。你可以看到一个“Hello World”信息和两个方块。

img/510438_1_En_1_Fig7_HTML.jpg

图 1-7

CRA MHL D3 "你好世界"

恭喜你,你刚刚创建了你的第一个集成了 React 和 D3 的项目。

HelloD3.tsx

现在,让我们检查一下HelloD3组件代码。

// src/components/HelloD3/HelloD3

从 React 开始,我们将使用useEffectrefObject库。

import React, { useEffect, RefObject } from 'react'

让我们导入样式文件;我们现在没有使用它,但它会准备好,以备我们需要添加一种风格。

import './HelloD3.scss'

接下来,我们将导入整个 D3 库。

import * as d3 from 'd3' // yarn add d3 @types/d3

对于我们的组件,我们将使用一个纯函数组件并设置我们的引用对象。

Refs 提供了访问在 render 方法中创建的 DOM 节点或 React 元素的方法。

https://reactjs.org/docs/refs-and-the-dom.html

const HelloD3 = () => {
 const ref: RefObject<HTMLDivElement> = React.createRef()

注意 TS 类型被设置为HTMLDivElement.我怎么知道呢?我不得不钻研 React 代码来找出ref对象类型。这是使用 TS 时的常见做法,我发现深入研究实际的 React 库很有帮助,因为它增加了我对 React 的理解。

接下来,我们将使用useEffect钩子来调用一个draw()方法,一旦调用了useEffect,我们将创建这个方法。

钩子是 React 16.8 中新增的。它们允许您使用状态和其他 React 特性,而无需编写类。

https://reactjs.org/docs/hooks-effect.html

 useEffect(() => {
  draw()
 })

我们的draw()方法包括“选择”或者选择我们将要使用的 HTML 元素。接下来,我们“追加”或者换句话说,添加一个文本元素“Hello World”

Note

在整本书中,我将经常使用draw()函数,而不在useEffect.中将draw()函数列为依赖函数。最好的方法是记住带有useCallback的绘制对象,以确保不会出现无限循环(参见 https://reactjs.org/docs/hooks-reference.html#usecallback )。如果你对这种方法不熟悉,我会在第 11 (使用useCallback)Memorize函数)一章中向你展示如何进行优化。

此外,我们选择将在 JSX 呈现的 SVG 元素,附加一个组元素和一个矩形元素,并转换 SVG 的宽度和填充颜色属性。我们的 SVG 大小为 250×500 像素。

 const draw = () => {
  d3.select(ref.current).append('p').text('Hello World')
  d3.select('svg').append('g').attr('transform', 'translate(250, 0)').append('rect').attr('width', 500).attr('height', 500).attr('fill', 'tomato')
 }

 return (

在 JSX 渲染方面,我们设置我们的div来保存我们用来添加文本元素的引用。

<div className="HelloD3" ref={ref}>

类似地,我们设置一个宽度和高度为 500px 的 SVG 元素,并在其中绘制一个矩形,用绿色填充该空间。

   <svg width="500" height="500">
    <g transform="translate(0, 0)">
     <rect width="500" height="500" fill="green" />
    </g>
   </svg>
  </div>
 )
}

export default HelloD3

这里发生的情况是,我们有一个 500×500 像素大小的矩形,然后 D3 覆盖了另一个 250×500 像素大小的矩形,这占用了 React JSX 矩形的一半大小。

这个例子很简单,没有显示为什么我们需要 D3,因为我们可以在 JSX 写这个文本和这些矩形元素,让我们的代码更可读。

然而,这是一个简单的极简“Hello World”,旨在帮助您理解工作部件,在本章的后面,您将看到d3.HelloD3.scss的威力。

我们的div包含了一个名为HelloD3className,它与我们的HelloD3.scss文件绑定在一起。

<div className="HelloD3" ref={ref}>

我们的 SCSS 文件内容目前只是一个占位符。

// src/components/HelloD3/HelloD3.scss

.HelloD3 {
}

我们的项目自带 SCSS,Webpack 已经配置了 SCSS 加载器,所以除了 CSS,你可以不用配置任何东西就可以使用 SCSS。

如果你以前没有用过 SCSS,你可能会问,为什么我用 SCSS 而不是 CSS?

CSS 预处理程序:Sass/SCSS

级联样式表(CSS)是 HTML 的核心功能,如果您还不熟悉 CSS,那么您需要熟悉它。这尤其适用于 HTML 和 React。在大型项目中,CSS 预处理程序通常用于补充 CSS 和添加功能。

React 项目通常使用四个主要的 CSS 预处理程序:Sass/SCSS、PostCSS、Less 和 Stylus。

Note

CSS 用于表示不同设备上网页的可视布局。CSS 预处理器通常用于增强 CSS 功能。

简单的回答是,萨斯/SCSS 对今天的大多数项目来说更好,所以我们将使用它。

调查显示,萨斯/SCSS 最受欢迎,可能会让你找到薪水最高的开发工作( https://ashleynolan.co.uk/blog/frontend-tooling-survey-2019-results )。萨斯/SCSS 被认为是一个众所周知的工具。如果你想了解更多,并查看不同 CSS 预处理程序之间的比较,请查看我在 Medium 上的文章: http://shorturl.at/dJQT3

HelloD3.test.tsx

正如我提到的,您已经有了一个为您自动创建的单元测试文件。

// src/component/HelloD3/HelloD3.test.tsx

import React from 'react'
import { shallow } from 'enzyme'
import HelloD3 from './HelloD3'

describe('<HelloD3 />', () => {
 let component

 beforeEach(() => {
  component = shallow(<HelloD3 />)
 })

 test('It should mount', () => {
  expect(component.length).toBe(1)
 })
})

如果您查看代码,您可以看到测试文件只测试了组件被安装(添加)到我们的显示中。

expect(component.length).toBe(1)

代码使用 Jest 和 Enzyme 库。Jest 自带 CRA 和酵素,Sinon 自带 MHL,所以你不需要做任何配置。

为什么 Jest 和酵素+ Sinon?

Jest 和酶+否则

Jest 是 JavaScript 单元测试框架,也是 React 应用的标准。它是为任何 JavaScript 项目而构建的,并且是 CRA 自带的。然而,我们确实需要 Jest-dom 和 Enzyme 来增强 Jest 的能力。

不然呢

另一个我们应该知道并添加到我们工具箱中的必备库是 Sinon ( https://github.com/sinonjs/sinon )。

Jest 和 Sinon 的目的是一样的,但是有时候你会发现一个框架对于特定的测试来说更自然、更容易使用。

我想尽早向您介绍测试,因为它是开发不可或缺的一部分,在构建接口时需要考虑。

$ yarn test

现在,为了运行测试,package.json文件已经配置了一个运行任务。

"test": "react-scripts test"

运行脚本时,可以看到测试通过,如图 1-8 所示。

img/510438_1_En_1_Fig8_HTML.jpg

图 1-8

单元测试通过

当您对组件进行更改时,请确保更新单元测试文件,并测试组件功能的不同部分。

我想指出的是,您的项目还附带了开箱即用的端到端(e2e)测试。

"test:e2e": "jest -c e2e/jest.config.js",
"test:e2e-alone": "node e2e/puppeteer_standalone.js",
"test:e2e-watch": "jest -c e2e/jest.config.js --watch"

正如我前面提到的,如果您想了解这些库是如何安装的,您可以创建自己的模板或修改一个模板。那超出了本书的范围;但是,您可以阅读本文,了解每个库的逐步安装过程:

https://medium.com/react-courses/setting-up-professional-react-project-with-must-have-reactjs-libraries-2020-9358edf9acb3

或者你可以参加我在 Udemy 上的课程,其中包括一个 40 页的电子书,帮助你理解所有的移动部件以及它们是如何安装和配置的:

https://www.udemy.com/course/getting-started-react17-with-must-have-libraries/

将类组件与 D3 React

在上一节中,我们创建了一个函数组件。如果我们想创建一个集成 D3 的 React 类组件,过程是类似的。我们可以使用我为您设置的模板作为起点:

$ npx generate-react-cli component HelloD3Class --type=d3class

就像 React 函数组件一样,d3class创建了三个文件。

  • HelloD3Class.scss

  • HelloD3Class.test.tsx

  • HelloD3Class.tsx

HelloD3Class.tsx

我们来回顾一下HelloD3Class.tsx

不同的是,我们先设置引用对象,再设置构造函数;然后,在构造函数级别,我们可以初始化引用对象。

一旦组件生命周期事件componentDidMount被调用,我们就可以将元素添加到 DOM 中。

看一看:

import React, { RefObject } from 'react'
import './HelloD3Class.scss'
import * as d3 from 'd3' // yarn add d3 @types/d3

export default class HelloD3Class extends React.PureComponent<IHelloD3ClassProps, IHelloD3ClassState> {
 ref: RefObject<HTMLDivElement>

 constructor(props: IHelloD3ClassProps) {
  super(props)
  this.state = {
   // TODO
  }
  this.ref = React.createRef()
 }

 componentDidMount() {
  d3.select(this.ref.current).append('p').text('Hello World')

  // const svg = d3.select(this.myRef.current).append('svg').attr('width', 500).attr('height', 500)
  d3.select('svg')
   .append('g')
   .attr('transform', 'translate(250, 0)')
   .append('rect').attr('width', 500)
   .attr('height', 500)
   .attr('fill', 'tomato')
 }

 render() {
  return (
   <div className="HelloD3Class" ref={this.ref}>
    <svg width="500" height="500">
     <g transform="translate(0, 0)">
      <rect width="500" height="500" fill="green" />
     </g>
    </svg>
   </div>
  )
 }
}

interface IHelloD3ClassProps {
 // TODO
}

interface IHelloD3ClassState {
 // TODO
}

Lint ESLint 和 beauty

进行代码审查,并让别人格式化你的代码以确保它的一致性,这有多好?

任何代码库中的所有代码都应该看起来像是一个人输入的,不管有多少人参与。

—瑞克·瓦德伦

幸运的是,这是可以做到的。

Lint 是一个分析代码的工具。它是一个静态代码分析工具,用来识别在代码中发现的有问题的模式。漂亮是一个固执己见的代码格式化程序。

Note

林挺是运行一个程序来分析你的代码以发现潜在错误的过程。

Lint 工具可以分析您的代码,并警告您潜在的错误。为了让它工作,我们需要用特定的规则来配置它。

争论每一行是否应该有两个空格,或者一个制表符、单引号、双引号等等,这是不明智的。这个想法是有一个风格指南,并遵循风格的一致性。正如有人说得好,

关于风格的争论毫无意义。应该有一个风格指南,你应该遵循它。

—丽贝卡·墨菲

Airbnb——作为其风格指南的一部分——提供了任何人都可以使用的 ESLint 配置,并成为标准配置。

ESLint 已经安装在 CRA MHL 模板上,但它优化了风格指南,你不需要做任何事情就能享受使用它的乐趣。

该项目已经使用 Airbnb 的风格指南(被认为是标准的)与 ESLint 和 Prettier for TypeScript 一起建立起来了。

然而,如果你想更好地理解,请阅读我在 https://medium.com/react-courses/react-create-react-app-v3-4-1-a55f3e7a8d6d 的文章,使用 Airbnb 的风格指南,用 ESLint 和 Prettier for TypeScript 设置你的项目。

为您配置了三个文件。

  • .eslintrc : ESLint 运行命令配置文件

  • .eslintignore斯洛文尼亚语忽略文件

  • .prettierrc:漂亮运行命令配置文件

package.json文件的运行脚本已经为您准备好了,所以我们可以运行lintformat实用程序,甚至只需一个命令就可以运行应用构建(我们将在本书后面介绍的生产构建)。

"scripts": {
  ..
  ..
  ..
  "lint": "eslint --ext .js,.jsx,.ts,.tsx src --color",
  "format": "prettier --write 'src/**/*.{ts,tsx,scss,css,json}'",
  "isready": "npm run format && npm run lint && npm run build"
}

我们已经准备好让lint完成它的工作并修改我们的代码,如图 1-9 所示。

img/510438_1_En_1_Fig9_HTML.jpg

图 1-9

运行 lint 命令后的输出

$ yarn run lint

要运行格式化程序来清理我们的代码,我们也可以使用yarn,如下所示(见图 1-10 ):

img/510438_1_En_1_Fig10_HTML.jpg

图 1-10

运行格式后的输出

$ yarn run format

摘要

在这一章中,我向你介绍了我们将在本书中用到的工具。

我们讨论了 React 及其优点和局限性。我们使用 CRA 和 MHL 模板项目建立了我们的初始项目,并研究了 TS、SCSS、单元测试、格式化和林挺。我还简要地提到了 D3、ReactTransitionGroup插件和其他可用的数据库。

我们用 React 和 D3 创建了我们的第一个“Hello World”项目,使用 TS 作为功能组件和类组件,我们通过测试、林挺和格式化来确保它的质量。

质量通常很重要,尤其是在使用图表和动画作为资源时。低质量的代码可能会导致用户体验下降,因为许多图表使用大型数据集(内存使用)以及大量 CPU 资源在客户端机器上呈现。

在下一章中,我们将继续学习如何创建 React 组件,这些组件利用 D3 来执行诸如绘制图形、创建动画和处理用户交互事件等任务。

二、图形和交互

在这一章中,我将向你展示你有哪些选择,并将创建图表的过程分解成更小的部分,这样你就可以在深入研究和创建图表之前更好地理解这个过程。这个过程可以分为三层:数据、视图和用户交互。

在这些层的任何部分,您都可以使用 React、D3 或任何其他与 React 集成的库。有选择是很棒的;然而,决定使用什么和何时使用也是令人困惑的。理解你的选择是很重要的,因为它能帮助你做出明智的决定。

这一章分为三个主要部分。

  • 制图法

  • 用户手势

  • 鼓舞

在本章的第一部分,我将展示如何使用 React 函数组件和类组件创建带有 HTML 和 SVG 元素的图形。我们将利用 React 的 JSX 以及 D3。我们将消耗数据和绘制元素。在本节的最后一部分,我们将创建第一个简单的图表。

在本章的第二部分,我将向你展示如何在 React 和 D3 里设置鼠标事件。

最后,我们将学习如何使用 React 制作动画,以及如何使用 D3 制作动画。

在本章结束时,你会明白在绘图、设置事件和制作动画时你有哪些选择。在这个过程中,我将向您展示设置函数和类组件的选项,并且我们将稍微讨论一下组件生命周期挂钩。还有,我会给你一些提示。

我们开始吧。

创建图表概述

正如我提到的,要创建数据可视化组件,这个过程可以分为三个主要层:数据、视图和用户交互。

让我们看看每一层都包括什么。

数据层由以下任务组成:

  • 获取数据。

  • 设定日期

  • 处理数据。

视图层由以下内容组成:

  • 与 React 生命周期挂钩集成。

  • 画一辆手推车

  • 设置组件的样式。

用户交互层包括以下内容:

  • 添加转场。

  • 处理用户手势。

绘制图表时,从数据开始是一个好的起点。你需要看到数据在讲述什么故事。一旦你理解了这个故事,就该选择一个图表了。您可以集成许多不同的现成图表库,也可以使用 D3 创建您的自定义解决方案。你的选择是无限的。不管你决定使用什么,理解所有东西是如何工作的是至关重要的。

绘制图形

你可能知道,React JSX 代码可能看起来很像 HTML,但它不是。这是 JavaScript 扩展(JSX)代码。

Note

JSX 是一个 React 扩展,它使用模仿 HTML 代码的 JavaScript 标签,因此代码大部分类似于 HTML,但它不是 HTML。

为了理解 React 为什么使用 JSX 而不仅仅是纯 HTML,我们首先需要谈谈文档对象模型(DOM)。React 会在后台处理您的 JSX 代码,然后将这些更改提交到用户的浏览器中,以加快用户页面的加载速度。

Note

文档对象模型(DOM)是 HTML 的内存表示,并且是树形结构。

React 努力匹配 HTML,JSX 可以识别 HTML 中支持的标签。SVG 就是一个很好的例子。我们在第一章中看到了如何将 SVG 标签添加到 React 渲染部分。

可缩放矢量图形(SVG)是一种基于 XML 的标记语言,用于描述基于二维的矢量图形。因此,它是一个基于文本的开放 web 标准,用于描述可以以任何大小清晰呈现的图像,并且专门设计为与其他 Web 标准(包括 CSS、DOM、JavaScript 和 SMIL)配合使用。本质上,SVG 对于图形就像 HTML 对于文本一样。

https://developer.mozilla.org/en-US/docs/Web/SVG

SVG 元素包括许多不同类型的图形,每个元素都有自己的属性集,从图像到圆形、线条、文本元素、矩形、组等等。即使是最有经验的 HTML 开发人员也不知道 SVG API 中的所有 SVG 元素和属性。

要了解关于 SVG 的更多信息,您可以在这里查看可用 SVG 标签的完整列表:

https://developer.mozilla.org/en-US/docs/Web/SVG/Element

我建议您将此页面加入书签,并在需要时参考。

SVG vsHTML

要画图形,SVG 是标准。SVG 已经成熟,在撰写本文时是第 2 版(从 2016 年开始)。您可以在此阅读第 2 版:

https://github.com/w3c/svgwg/wiki/SVG-2-new-features

然而,我想指出还有其他方法来画图形。例如,我们可以通过 JavaScript 使用 HTML <canvas>元素。元素是一个图形容器。

SVG 对于较少数量的对象或较大的表面提供了更好的性能。对于较小的表面或较大数量的对象,画布可以提供更好的性能。

画布可能会变得模糊,需要检查和调整设备像素比率。为了避免不同设备像素比率的模糊图像,请在此处查看 Mozilla 文档:

https://developer.mozilla.org/en-US/docs/Web/API/Window/devicePixelRatio

下面是使用 canvas 和 SVG 之间的区别的简要概述:

  • SVG 允许用 CSS 进行样式化;画布只能通过脚本来更改。

  • SVG 是基于向量的;画布是基于光栅的(矩形像素网格)。

  • SVG 提供了比画布更好的可伸缩性,所以一旦一个元素需要缩放,SVG 是首选。

  • SVG 比 canvas 具有更大的屏幕和更少的对象的性能优势;但是,如果您使用较小的屏幕尺寸和许多对象,画布会更好。

JSX Canvas(密西西比州)

画布( https://www.w3schools.com/html/html5_canvas.asp )可以用来绘制 HTML 中的组件。事实上,您不仅可以使用画布来绘制组件,还可以将它制作成动画并操纵元素属性。React JSX 版本的画布没有什么不同。

让我们创造一个例子。我们可以使用我们在第一章 ( react-d3-hello-world)中创建的同一个项目,并添加一个包含画布的组件。你可以从该书的资源库下载本章所有示例的最终代码。

https://github.com/Apress/integrating-d3.js-with-react/tree/main/ch02

JSXCanvas.tsx

我们将组件称为JSXCanvas。你可以自己创建文件,或者使用我为你设置的模板从generate-react-cli那里获得帮助。

npx generate-react-cli component JSXCanvas --type=d3

在我们的JSXCanvas组件中,我们将使用画布 JSX 组件,它匹配 HTML 画布。我们将改变属性来改变画布的颜色为番茄红。

让我们看一下代码。

// src/component/JSXCanvas/JSXCanvas.tsx

首先,我们导入将要使用的 React 库和 SCSS 文件。

import React, { RefObject, useEffect, useRef } from 'react'
import './JSXCanvas.scss'

我们将使用ref对象和useEffect。我们首先创建对画布的引用。

当涉及到 TypeScript 时,我们需要定义对象的类型(当类型不能通过赋值清楚地推断出来时)。对于画布,它的类型应该是HTMLCanvasElement

const JSXCanvas = () => {

 const canvasRef: RefObject<HTMLCanvasElement> = useRef(null)

一旦组件被初始化,我们就可以设置useEffect方法。

什么是useEffectuseEffect在渲染过程完成后调用。useEffect检查上一次渲染的依赖值,如果其中任何一个发生变化,将调用你的效果函数。

在我们的例子中,当这种情况发生时,我们可以画出我们的画布。我们在这里使用 React 函数组件,设置一个 draw 方法是一个很好的方法,而不是仅仅在useEffect中编写代码,因为该函数可以被其他方法使用。然而,在某些情况下,在useEffect中编写实际代码会更有意义,这一点您将在本书后面看到。

  useEffect(() => {
    draw()
  })

我们的 draw 方法将使用我们创建的对<canvas>元素的引用,然后使用画布上下文,我们可以设置画布属性,如widthheightcolor。CanvasRenderingContext2D 接口是 Canvas API 的一部分,它为 Canvas 元素的绘制表面提供了 2D 渲染上下文。它用于绘制形状、文本、图像和其他对象。

https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D

看一看:

  const draw = () => {
    const canvas = canvasRef.current
    const context = canvas?.getContext('2d')
    if (context) {
      context.fillStyle = 'tomato'
      context.fillRect(0, 0, context.canvas.width, context.canvas.height)
    }
  }

最后,在渲染方面,我们将使用我们创建的引用来设置 JSX 画布。

  return (
    <>
      <canvas ref={canvasRef} />

    </>
  )
}
export default JSXCanvas

还有另一种方法可以编写更少的关于引用的代码。我们可以内联的方式来完成,而不是定义变量然后在组件中赋值。让我们来看看。

我们设定了ref

ref: SVGCircleElement | undefined

接下来,我们可以在 JSX 代码中内联赋值ref

<canvas
  ref={canvasRef} />
  // eslint-disable-next-line no-return-assign
  ref={(ref: SVGCircleElement) => (this.ref = ref)}
/>

然而,这不是最干净的代码,因为根据最佳编码实践,我们不应该返回赋值;然而,这就是为什么我通过添加一个eslint评论来禁止 ESLint 抱怨。但是,由于代码的模糊性,不推荐这种方法;我想让你看看这些选项。

App.tsx

记得将组件添加到您的App.tsx父组件中。

// src/App.tsx

function App() {
  return (
    <div className="App">
      <header className="App-header">
        <JSXCanvas />
      </header>
    </div>
  )
}

要查看图形,请确保您仍在第一章中的端口 3000 上运行。

$ yarn start

图 2-1 显示了最终结果。

img/510438_1_En_2_Fig1_HTML.jpg

图 2-1

根据我们选择的尺寸和颜色进行画布绘制

缩放怎么样?是的,您可以使用缩放并根据屏幕大小调整画布,以避免画布放大后变得模糊。为此,使用context.scale

const { devicePixelRatio: ratio = 1 } = window
context.scale(ratio, ratio)

但是,如果您希望在所有设备上获得清晰的打印质量,或者您需要缩放等功能,SVG 会提供更好的结果。我不打算深入研究画布,因为我建议继续使用 SVG 然而,我想让你知道这是可能的,如果你曾经有一个用例需要一个画布,比如在一个针对较小屏幕尺寸和许多对象的用例中,你会在你的工具箱中有它。

React SVG

要用 React 创建 SVG 图形,让我们添加另一个组件,名为HelloSVG

npx generate-react-cli component HelloSVG --type=d3

generate-react-cli模板已经包含了在 React 中显示 SVG 元素所需的代码。看一看:

import React from 'react'
import './HelloSVG.scss'

const HelloSVG = () => {

  return (
    <div className="HelloSVG">
      <svg width="500" height="500">
        <g transform="translate(0, 0)">
          <rect width="300" height="300" fill="tomato" />
        </g>
      </svg>
    </div>
  )
}

export default HelloSVG

注意,我使用了transform属性。转换将组元素移动到左上角。您可以在 Mozilla 文档中了解更多关于transform属性的信息。

https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/transform

现在,如果我们想改变矩形的设计属性,而不使用fill属性用颜色填充 SVG 矩形。React 为 HTML 类提供了一个匹配的属性( https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/class ): className。我们可以指定一个类名,并在我们的 SCSS 文件中定义它。这并不新鲜,所有的 React 组件都是这样工作的。

// HelloSVG.tsx
<rect className="myRect" width="300" height="300" />

// HelloSVG.scss
.myRect {
  fill: #ba2121;
}

最后,我们需要将组件添加到我们的入口点App.tsx

// src/App.tsx

<HelloSVG />

图 2-2 显示了我们的 SVG 矩形元素。

img/510438_1_En_2_Fig2_HTML.jpg

图 2-2

使用 SVG 绘制矩形

属性可以用于很多事情,比如旋转、缩放和倾斜。

例如,如果我们想实现 Mozilla docs ( https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/transform )中的例子。该示例使用xlink将一个 SVG 路径分配给一个元素以供重用,从而创建一个心形和一个阴影效果。不幸的是,你经常会看到,如果没有一定程度的修改和知识,你不能将你在网上看到的大多数代码(无论是否与 D3 相关)复制粘贴到 React 中。

在我们的例子中,Mozilla 示例无法编译的原因是 React SVG 不包含use xlink,因此这段代码将生成一个错误:

<use xlink:href="#heart" fill="none" stroke="white"/>

代码将生成错误“Type { xlink:true;}不可赋给类型 SVGProps 。

该错误是由于 React SVGUseElement在 React 的当前版本(撰写本文时为版本 17)中不包含xlink而导致的。然而,不要惊慌,因为这不是世界末日。我们需要做的是定义标签,然后我们就可以使用它了。

const useTag = '<use xlink:href="#heart" />'

接下来,我们可以用一个 SVG 路径(在我们的例子中是一个心脏的形状)建立一个组,转换属性将使心脏看起来像一个阴影。

<g
  fill="grey"
  transform="rotate(-10 50 100)
    translate(-36 45.5)
    skewX(40)
    scale(1 0.5)"
>
  <path id="heart" d="M 10,30 A 20,20 0,0,1 50,30 A 20,20 0,0,1 90,30 Q 90,60 50,90 Q 10,60 10,30 z" />
</g>

然后我们可以使用我们在 SVG 标签中设置的useTag常量来拖动实际的心脏。

<svg dangerouslySetInnerHTML={{__html: useTag }} fill="none" stroke="white" />

使用dangerouslySetInnerHTML完成对useTag的赋值。

dangerouslySetInnerHTML听起来很吓人,顾名思义,你应该小心使用它,因为恶意代码可能会被注入到那个标签中。这就是为什么你需要添加eslint评论来防止 ESLint 对这个消息的攻击。

{/* eslint-disable-next-line react/no-danger */}

在我们的例子中,我们知道我们在做什么,我们没有传递一些可能导致注入攻击的运行时字符串,所以我们很好。

看一下整个代码块:

const HelloSVG = () => {
  const useTag = '<use xlink:href="#heart" />'
  return (
    <div className="HelloSVG">
      <svg width="500" height="500">
        <g transform="translate(0, 0)">
          <rect className="myRect" width="300" height="300" /* fill="tomato" */ />
        </g>
        <g
          fill="grey"
          transform="rotate(-10 50 100)
            translate(-36 45.5)
            skewX(40)
            scale(1 0.5)"
        >
          <path id="heart" d="M 10,30 A 20,20 0,0,1 50,30 A 20,20 0,0,1 90,30 Q 90,60 50,90 Q 10,60 10,30 z" />
        </g>
        {/* eslint-disable-next-line react/no-danger */}
        <svg dangerouslySetInnerHTML={{__html: useTag }} fill="none" stroke="white" />
      </svg>
    </div>
  )
}

输出为我们创建了心脏轮廓和阴影,如图 2-3 所示。如果我想给阴影添加一个轮廓,那不成问题。我们可以将相同的 SVG 代码放在 group 元素中。让我们继续试一试。

img/510438_1_En_2_Fig3_HTML.jpg

图 2-3

通过 xlink 使用 SVG

使用 JSX 在 React 中映射数据

为了使它比仅仅使用 SVG 更加真实和有趣,我们现在想使用 React props传递来自父组件的数据,并使用带有一些简单 React 代码的 HTML p标签来绘制数据。让我告诉你怎么做。

HelloJSXData.tsx

自己或者用模板创建一个新组件,并将其命名为HelloJSXData

npx generate-react-cli component HelloJSXData --type=d3

接下来,导入风格 SCSS 和 React。

// src/component/HelloJSXData/HelloJSXData.tsx

import React from 'react'
import './HelloJSXData.scss'

接下来,设置我们将传递给子组件的props。我指向一个我将在文件底部定义的接口。

const HelloJSXData = ( props : IHelloJSXDataProps ) => {

在 JSX 渲染方面,我将映射数组并从父组件传递它。一旦数据被映射,我们就可以使用p HTML 标签在屏幕上显示数据。看一看:

  return (
    <div className="HelloJSXData">
      {props.data.map((d, index) => (
        <p key={`key-${  d}`}>
          jsx {d}
        </p>
      ))}
    </div>)
}

这是我们对props的接口,只是传递一个由字符串组成的数组:

interface IHelloJSXDataProps {
  data: string[]
}

export default HelloJSXData
App.tsx

和往常一样,记得给 app 加上HelloJSXData

// src/app
function App() {
  return (
    <div className="App">
      <header className="App-header">
        <HelloJSXData data={['one', 'two', 'three', 'four']} />
      </header>
    </div>
  )
}

参见图 2-4 中的最终结果。

img/510438_1_En_2_Fig4_HTML.jpg

图 2-4

使用 React 代码映射数据示例

我将向您展示如何使用p HTML 标签来简化事情,但是这也可以是一个 SVG 元素,比如一个圆形或一个矩形,正如我将在其他示例中展示的那样。

使用 D3 在 React 中映射数据

前面的例子是纯粹、简单的 React 代码。在这个例子中,我们添加 D3 来为我们添加地图和绘图。我们姑且称这个组件为HelloD3Data

HelloD3Data.tsx
npx generate-react-cli component HelloD3Data --type=d3

我们将使用 D3,所以确保你已经有了 D3 和导入的类型,如果你以前没有为这个项目做过的话。

yarn add d3 @types/d3

让我们从定义将要使用的导入开始。

// src/component/HelloD3Data/HelloD3Data.tsx

import React, { useEffect } from 'react'
import './HelloD3Data.scss'
import * as d3 from 'd3'

和以前一样,我们将设置我们的props接口来传递相同的字符串数组。

const HelloD3Data = ( props : IHelloD3DataProps ) => {

  useEffect(() => {
    draw()
  })

当我们使用 D3 时,我们需要首先选择我们将在 JSX 渲染中设置的元素。接下来,通过使用 d3 selectAll API,我们可以选择 p HTML 元素和data属性。然后我们可以传递关于text属性的数据。我将使用 inline 函数将文本设置为数据值。看一看:

  const draw = () => {
    d3.select('.HelloD3Data')
      .selectAll('p')
      .data(props.data)
      .enter()
      .append('p')
      .text((d) => `d3 ${  d}`)
  }

对于我们的渲染,我们将设置一个div作为我们的p标签的包装器。

  return <div className="HelloD3Data" />
}

interface IHelloD3DataProps {
  data: string[]
}

export default HelloD3Data

最后,让我们设置父组件。

// src/app
<HelloD3Data data={['one', 'two', 'three', 'four']} />

你可以在图 2-5 中看到结果。

img/510438_1_En_2_Fig5_HTML.jpg

图 2-5

使用 D3 和 React 代码映射数据示例

如你所见,当比较 React 和 D3 之间数据迭代的两个例子时,D3 看起来不如 React 直观。我们的例子本质上很简单,但是当需要大量计算和大型数据集时,D3 将会大放异彩。

带 D3 的简单条形图

到目前为止,我们已经处理了简单的例子,并操作了 HTML 元素,如<canvas>p标签和div。此外,我们还使用了 SVG 和 D3。

现在我们准备用 D3 创建我们的第一个数据可视化图表,并进行 React。我们的例子与上一个例子相似。我们获取数据,并使用 D3 中的 data 属性,我们将遍历我们的数据,并使用 rectangle SVG 元素来绘制条形,就像我们使用p HTML 标签一样。看一看:

// src/component/SimpleChart/SimpleChart.tsx

import React, { RefObject } from 'react'
import './SimpleChart.scss'
import * as d3 from 'd3'

这一次,让我们用一个类组件而不是函数组件来创建图表,这样您就可以看到不同之处了。

我想指出的是,当你不需要shouldComponentUpdate生命周期事件时,最好扩展 React PureComponent

Tip

React.PureComponent在某些情况下提供了性能提升,但代价是失去了shouldComponentUpdate生命周期挂钩。你可以在 React 文档( https://reactjs.org/docs/react-api.html#reactpurecomponent )中了解更多。

export default class Component extends React.PureComponent<ISimpleChartProps, ISimpleChartState> {

我将保存对我将用来包装图表的div的引用,并将数据保存在 numbers 类型的数据数组中。

  ref: RefObject<HTMLDivElement>

  data: number[]

在类构造函数中,我设置接口,创建引用,并设置数据。

请注意,我并不需要状态,但我正在设置它,以防将来需要它。这是一个好习惯。

  constructor(props: ISimpleChartProps) {
    super(props)
    this.state = {
      // TODO
    }
    this.ref = React.createRef()
    this.data = [100, 200, 300, 400, 500]
  }

正如您在前面的函数组件示例中回忆的那样,我们称之为draw函数。在 React 类组件中,我们可以在componentDidMount生命周期挂钩期间调用draw方法。

Tip

componentDidMount()生命周期挂钩是安装阶段的一部分,可以被类组件覆盖。在第一次安装组件时,在一个componentDidMount()生命周期钩子中定义的任何动作只被调用一次。

  componentDidMount() {
    this.drawChart()
  }

drawChart方法上,我会借助 D3。我首先选择引用,然后使用 append 添加一个 SVG 元素,最后设置我们的宽度和高度。

由于我设置了data属性,D3 将自动遍历数据来绘制我们所有的矩形。这与我们在前面的例子中使用p标签是一样的,但是这里我们使用一个矩形标签来代替 HTML p标签。看一看:

  drawChart() {
    const size = 500
    const svg = d3.select(this.ref.current)
    .append('svg')
    .attr('width', size)
    .attr('height', size)

    const rectWidth = 95
    SVG
      .selectAll('rect')
      .data(this.data)
      .enter()
      .append('rect')
      .attr('x', (d, i) => 5 + i * (rectWidth + 5))
      .attr('y', (d) => size - d)
      .attr('width', rectWidth)
      .attr('height', (d) => d)
      .attr('fill', 'tomato')
  }

为了渲染,我正在设置我们将使用的div包装器。注意,我并不真的需要引用,因为我可以使用className选择元素。在本例中,这是一个由您决定什么更直观的偏好问题。

  render() {
    return <div className="SimpleChart" ref={this.ref} />
  }
}

Tip

在一些例子中,我们在元素类名上使用 D3 select,而在其他例子中使用引用。在我们的例子中,使用类名作为引用很好;然而,在有些情况下,我们希望 React 控制我们的组件,这就是设置引用更好的地方,因为 React 知道元素中的变化。这种情况会出现在列表中:array.map((item: object) => ( <ListItem item={item} /> ))}。React 遍历一个列表,如果我们使用一个 map 来呈现这个列表,而这个组件没有使用 reference,那么计算就会被关闭。

最后,正如我之前所做的,我正在定义props和状态,在这个例子中我不需要它们,但是我在这个例子中设置它们,因为它们在将来可能会被需要。

interface ISimpleChartProps {
  // TODO
}

interface ISimpleChartState {
  // TODO
}
App.tsx

<SimpleChart />组件添加到App.tsx,就像我们在前面的例子中所做的一样。你可以在图 2-6 中看到最终的结果。

img/510438_1_En_2_Fig6_HTML.jpg

图 2-6

使用 React 和 D3 的简单条形图

祝贺您,您刚刚创建了您的第一个图表—条形图!

React 组件生命周期挂钩

当谈到 React 时,你真的想熟悉 React 的 16.9 版和更高的生命周期挂钩,因为它们自 16.9 版以来发生了变化。深刻理解 React 的生命周期挂钩有助于确保仅在需要时才更改 DOM,以更好地配合 React 的 VDOM 范式,并确保您的组件得到优化。

React 中的每个组件都有一个生命周期,您可以在它的三个主要生命周期阶段对其进行监控和操作。

  • 挂载阶段:组件被创建来开始它的单向数据流之旅,并到达 DOM。它使用constructor(),以及以下方法:静态getDerivedStateFromProps()render()componentDidMount()

  • 更新阶段:组件被添加到 DOM 中,更新可以在属性或状态改变时重新呈现。事件有:静态getDerivedStateFromProps()shouldComponentUpdate()render()getSnapshotBeforeUpdate()componentDidUpdate()

  • 卸载阶段:这是组件生命周期的最后一个阶段。组件被销毁并从DOM.componentWillUnmount()移除。

在 React 17 中,对以前的方法如componentWillMount的访问被否决了,你应该使用新的钩子来访问组件生命周期。包括getDerivedStateFromPropsgetSnapshotBeforeUpdatecomponentDidMountcomponentDidUpdate

您可能需要重温您的生命周期挂钩知识,或者只是看看您可以用 React 创建的所有不同类型的类和函数组件。这超出了本书的范围,但是我强烈推荐您在这里阅读我的文章:

https://medium.com/react-courses/react-component-types-functional-class-and-exotic-factory-components-for-javascript-1a098a49a831

用户手势

用户与图形的交互与用户与任何 React 组件的交互没有什么不同。当涉及到鼠标事件时,React JSX 支持 HTML 和 SVG。

但是,您仍然可以选择如何利用这些事件。

您可以使用 JSX 事件或使用 D3。何时使用工具取决于您正在构建什么。如果您使用 D3 来创建图形,您将需要也使用 D3 事件;然而,如果你使用 JSX 创建你的图形,你可以使用 JSX 或 D3。选择权在你。

出于这些原因,我将向您展示这两种方法。让我们来看看。

对鼠标事件做出 React

让我们创建一个类组件,并将其命名为CircleWithEvents

// src/component/CircleWithEvents/CircleWithEvents.tsx

import * as React from 'react'
import './CircleWithEvents.scss'

export default class CircleWithEvents extends React.PureComponent<ICircleWithEventsProps> {

  componentDidMount() {
    // TODO
  }

接下来,设置事件处理程序。我只是设置了一个警报来显示事件被调度。代码被注释掉了,供您随意使用。

  onMouseOverHandler(event: React.MouseEvent<SVGCircleElement, MouseEvent>) {
    // alert('onMouseOverHandler')
  }

  onMouseOutHandler() {
    // alert('onMouseOutHandler')
  }

注意,我使用的是React.MouseEvent而不是标准 HTML 的MouseEvent

Tip

在 React 中,事件是对鼠标悬停、鼠标点击、按键等特定动作的触发 React。在 React 中处理事件类似于在 DOM 元素中处理事件。但是有一些句法上的差异。

synthetic event*,围绕浏览器原生事件的跨浏览器包装器。它与浏览器的本机事件具有相同的接口,包括 stopPropagation()和 preventDefault(),只是这些事件在所有浏览器中的工作方式相同。*

https://reactjs.org/docs/events.html

事件使用 camel case 命名,而不是仅仅使用小写字母。这里举两个例子:React.KeyboardEventReact.MouseEvent

事件作为函数而不是字符串传递。如果您遵循代码,您可以看到 React 事件扩展了UIEvents,它扩展了SyntheticEvent

React 使用自己的事件系统。这就是为什么我们通常不能使用标准 DOM 中的MouseEvent

我们需要使用 React 的事件;否则,我们会得到一个错误,或者我们不能访问方法。一般来说,大多数事件都以相同的名称映射。

幸运的是,React 的类型为您提供了标准 DOM 中您可能熟悉的每一个事件的适当等价物。

我们可以使用React.MouseEvent或者从 React 模块导入MouseEvent类型。

在渲染方面,我将设置一个空的包装器标签,如,一个 SVG 组,一个半径(r)为 100 像素的圆,以及鼠标事件。

  render() {
    return (
    <>

        <svg width="500" height="500">
          <g>
            <circle
              className="circle"
              transform="translate(150 150)"
              r="100"
              onMouseOver={(event) => {
                event.stopPropagation()
                this.onMouseOverHandler(event)
              }}
              onMouseOut={(event) => {
                event.stopPropagation()
                this.onMouseOutHandler()
              }}

对于点击事件,让我们做一些不同的事情。我将使用内嵌函数(arrow 函数,又名 fat 函数)来显示鼠标点击事件的警告。

              onClick={(event) => {
                event.stopPropagation()
                // eslint-disable-next-line no-alert
                alert('onClick')
              }}
            />
          </g>
        </svg>
       </>

    )
  }
}
interface ICircleWithEventsProps {
  // TODO
}

注意,我的 SVG 圆圈包含一个名为circleclassName属性,在这里我将设置一些 CSS 属性,比如光标指针的显示、圆圈的宽度和高度以及灰色填充颜色。这些属性可以在组件本身上设置,但是这样代码更简洁。

// src/component/CircleWithEvents/CircleWithEvents.scss

.circle {
  cursor: pointer;
  width: 150px;
  height: 150px;
  fill: #6666;
}
App.tsx

最后,像往常一样,记住将组件添加到App.tsx。继续点击圆圈;不要害羞。

// src/App.tsx

import CircleWithEvents from './components/CircleWithEvents/CircleWithEvents'

<CircleWithEvents />

参见图 2-7 中的最终结果。

img/510438_1_En_2_Fig7_HTML.jpg

图 2-7

用鼠标事件圈出 SVG

D3 鼠标事件

在下一个示例中,我将复制上一个示例中的相同功能,用鼠标事件设置一个圆圈;然而,这一次,我将使用 D3 代替 React JSX。让我们来看看。

设置importsclass组件。

// src/component/CircleWithEvents/CircleWithD3Events.tsx

import * as React from 'react'
import './CircleWithEvents.scss'
import * as d3 from 'd3'

export default class CircleWithD3Events extends React.PureComponent<ICircleWithD3EventsProps> {

一旦调用了componentDidMount事件,我将调用我的draw()方法。

  componentDidMount() {
    this.draw()
  }

接下来,我将设置与上一个示例相同的事件处理程序。

  onMouseOverHandler(event: React.MouseEvent<SVGCircleElement, MouseEvent>) {
    // alert('onMouseOverHandler')
  }

  onMouseOutHandler() {
    // alert('onMouseOutHandler')
  }

我的draw()方法将选择我的 SVG 包装元素,然后添加一个组元素和一个圆形元素,并使用 D3 上的.on方法设置事件。

在这个例子中,我甚至附加了同一个类圈。

Note

React JSX 调用类属性className,在 D3 属性匹配 HTML SVG,所以它会是class

  draw = () => {
    d3.select('svg')
      .append('g')
      .append('circle')
      .attr('transform', 'translate(150, 150)')
      .attr('r', 100)
      .attr('class', 'circle')
      .on('click', () => {
        alert('onClick')
      })
      .on('mouseover', (event) => {
        this.onMouseOverHandler(event)
      })
      .on('mouseout', (event) => {
        this.onMouseOutHandler()
      })
  }

如您所料,这是我在渲染端的 SVG 包装器:

  render() {
    return (
    <>

        <svg id="svg" width="500" height="500" />

    </>
    )
  }
}

interface ICircleWithD3EventsProps {
  // TODO
}
App.tsx

添加我们的组件App.tsx,你会看到与图 2-7 相同的结果。

// src/App.tsx

import CircleWithD3Events from './components/CircleWithEvents/CircleWithD3Events'

<CircleWithEvents />

你可以看到,对于大多数人来说,React JSX 更直观,更容易阅读;但是,如果你需要写 D3 代码,你需要使用 D3 事件,一旦你明白你在做什么,它会很简单。

了解这两个选项会给您带来灵活性,并对您的代码有更多的控制。当我们希望 d3 负责我们组件的 DOM 时,最好使用 React 元素并尽可能多地绑定 React。在我们需要接管处理 DOM 任务的情况下,拥有 d3 可以帮助我们。

动画图形

在这一部分,我将介绍交互层的另一部分:创建动画。当我们改变数据或显示数据随时间的变化时,我们可能希望创建一个过渡,我们可以用动画或过渡以更优雅的方式来实现。

React 提供了许多创建转场的方法。以下是一些例子:

  • CSS 转场:我们可以使用普通的旧 CSS,将 CSS 转场作为样式表的一部分,或者动态地使用style={{someCSSPropertyExample: 50}}

  • CSS-in-JS 模块:如果使用 CSS-in-JS 模块之类的库,可以用直观的方式设置过渡。

  • D3 转换 : D3 也使用转换 API 提供转换。

当我们开发图表时,我们将使用过渡,正如我们在事件中看到的那样,知道我们的选项是很好的。

我将向您展示 CSS-in-JS 模块和 D3。

使用 React 制作动画

CSS-in-JS 模块是设计 React 应用的流行选项,因为它们与 React 组件紧密集成,并具有允许您更改从父组件传递的属性或从状态绑定属性的功能。

事实上,CRA·MHL 图书馆项目是用现成的 Material-UI 和样式组件建立的,所以你不需要安装任何东西;我们可以开始使用材质 UI 和样式组件。

例如,我们可以基于 React props改变我们的风格。此外,默认情况下,这些系统中的大多数都将所有样式的范围扩展到被样式化的相应组件。

当谈到 CSS-in-JS 模块时,有许多选项可供选择,如样式化组件、情感和样式化 jsx。

我选择了风格化组件,因为它与 Material-UI 密切相关;你可以在 https://styled-components.com 了解更多(如果你不熟悉的话)。

但是等等,为什么我们需要样式化的组件?Material-UI 不是已经有了类似于样式化组件的样式化导入吗?

是的,Material-UI 是一个很棒的组件库,它模仿了 Google 的材质设计,并内置了样式机制。那么为什么还不够呢?简单的回答是,Material-UI CSS-in-JS 解决方案感觉不如样式化组件强大。Material-UI 从一开始就被设计成使用自己的样式解决方案 CSS-in-JS。但是有时你想要其他在 Material-UI 风格中没有的特性,或者你像我一样,只是更喜欢风格化的组件。

幸运的是,Material-UI 确实简化了其他样式解决方案的使用。

Styled Components 是另一个用于样式化 React 组件的伟大库。这是通过定义没有 CSS 类的 React“样式化”组件来实现的。当您想要编写常规 CSS、传递函数和props时,最好使用样式化组件。你可能会问,为什么不直接使用样式化的组件呢?

在撰写本文时,还没有很多成熟的库处于 Material-UI 水平,当然也没有达到 Material-UI 成熟度水平。

我们可以同时利用 Material-UI 和 Styled 组件的优点。

使用样式化组件将为您提供以下能力:

  • 使用props有条件地渲染 CSS

  • SCSS 支持

  • CSS 的模板文字语法

还有更多。

对于我们将在本书后面使用的一个图表,我们需要一个脉动的圆。跳动的圆圈可以用来突出显示图表上的某些内容。

脉动圈. tsx

为了使用样式组件库创建那个脉动的圆形组件,让我们创建一个新的功能组件,并将其命名为PulsatingCircle.tsx

// src/component/PulsatingCircle/PulsatingCircle.tsx

import React from 'react'

我们从样式组件中导入关键帧和样式。

import styled, { keyframes } from 'styled-components'

现在,看看编写可以通过传递参数来更新的动态代码的能力。让我们定义一个采用两种颜色的函数。

const circlePulse = (colorOne: string, colorTwo: string) => keyframes`
0% {
  fill:${colorOne};
  stroke-width:20px
}
50% {
  fill:${colorTwo};
  stroke-width:2px
}
100%{
  fill:${colorOne};
  stroke-width:20px
}

`

现在使用动画,我们可以创建一个无限的四秒钟线性循环来形成这种脉动效果。

const StyledInnerCircle = styled.circle`
  animation: ${() => circlePulse('rgb(245,197,170)', 'rgba(242, 121, 53, 1)')} infinite 4s linear;
`

export default function PulsatingCircle(props: IPulsatingCircle) {

在渲染方面,我们可以使用作为 JSX 组件创建的StyledInnerCircle

注意,我从props开始给 x,y 赋值。父组件可以传递这些。

  return (
  <>

      <StyledInnerCircle cx={props.cx} cy={props.cy} r="8" stroke="limegreen" stroke-width="5" />
  </>

  )
}

在我们的props接口端,我们可以传递自定义组件的位置。

interface IPulsatingCircle {
  cx: number  cy: number
}

将我们的组件添加到App.tsx

// src/app.tsx

<svg width={400} height={400} viewBox="0 0 800 450">
  <g>
    <PulsatingCircle cy={100} cx={100} />
  </g>
</svg>

你现在有了一个脉动的动画圆,如图 2-8 所示。

img/510438_1_En_2_Fig8_HTML.jpg

图 2-8

使用 React 样式组件的脉动圆

用 D3 制作动画

就用 D3 制作动画而言,这个过程遵循一个动画序列。如果你熟悉任何动画软件,你会发现 D3 更直观,因为你了解动画的概念。对其他人来说,这可能会令人困惑。

脉动循环 3.tsx

作为一个例子,让我们创建另一个脉动圆,但这次让我们使用 D3。

导入并设置绘制方法。

import React, { useEffect } from 'react'
import * as d3 from 'd3'

const PulsatingCircleD3 = () /* props */ => {
  useEffect(() => {
    drawPulsatingCircle()
  })
  const drawPulsatingCircle = () => {

为了保持循环,我将创建一个名为repeat()的函数。该功能将选择circlSVGvg元素(我正在 JSX 创建圆;现在你知道怎么做了,如果你想得到 100%纯的 D3)。

接下来,我将设置一个过渡。

在第一个 300 毫秒期间,我首先将笔画的属性设置为 0,然后设置持续时间并将笔画不透明度从 0 更改为 0.5。

接下来,我设置另一个笔画来改变,并在动画中使用 D3 缓动来创建正弦缓动(是的,就是我们在数学课上使用的正弦圆)。

最后,函数使用递归函数(调用自身的函数)调用自身进行循环。

    (function repeat() {
      d3.selectAll('.circle')
        .transition()
        .duration(300)
        .attr('stroke-width', 0)
        .attr('stroke-opacity', 0)
        .transition()
        .duration(300)
        .attr('stroke-width', 0)
        .attr('stroke-opacity', 0.5)
        .transition()
        .duration(1000)
        .attr('stroke-width', 25)
        .attr('stroke-opacity', 0)
        .ease(d3.easeSin)
        .on('end', repeat)
    })()
  }

在 JSX,我正在创建一个半径为 8 像素,x,y 的位置为 50,50 的圆,如图 2-9 所示。

img/510438_1_En_2_Fig9_HTML.jpg

图 2-9

使用 D3 的脉动圆

  return (
  <>

      <svg>
        <circle className="circle" cx="50" cy="50" stroke="orange" fill="orange" r="8" />
      </svg>
  </>

  )
}
export default PulsatingCircleD3

你可以看到这个想法是为了给动画排序。我肯定有些人会觉得这更复杂,而其他人会觉得这比上一个例子简单。

摘要

本章分解了开始用 React 和 D3 创建图表所需要知道的内容。这一章分成三个主要部分。

  • 制图法

  • 用户手势

  • 鼓舞

在第一部分中,我向您展示了如何使用 React 函数和类组件用 HTML 和 SVG 元素创建图形。

我们同时使用了 JSX 和 D3。我们消耗数据,绘制元素。我们甚至创建了我们的第一个图表,一个条形图。

在第二部分中,您学习了用 D3 和 React 设置事件,以及用 React 和 D3 制作动画。

如你所见,在绘图、消耗数据、制作动画、甚至与选项交互时,你可以使用 React、D3 或其他库。这也是 D3 和 React 的独特之处。当需要将您的图表集成到现有代码中、对其进行测试,以及与拥有不同技能的不同成员团队合作时,这些选项集真的很方便;它甚至使你的代码更具可读性。

在下一章,我们将开始创建简单的图表,如折线图、面积图和条形图,以及消费数据、动画和与这些图表互动。