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

173 阅读14分钟

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

原文:Integrating D3.js with React

协议:CC BY-NC-SA 4.0

三、基本图表:第一部分

在前一章中,我介绍了 D3 和 React 的可能性。我们创建了函数和类组件,甚至创建了一个简单的条形图。在这一章中,我将介绍如何使用 TypeScript 作为类型检查器,用 React 和 D3 创建简单的图表。我将向您展示如何创建下面的简单图表,重点是让 D3 完成大部分工作。我将向您展示如何创建以下图表:

  • 折线图

  • 对比图

  • 条形图

我们开始吧。

设置

正如前几章一样,我将使用 CRA 和 MHL 模板来创建我们的起始项目。

$ yarn create react-app basic-charts --template must-have-libraries

$ cd basic-charts
$ yarn add d3 @types/d3
$ yarn start

打开起始页。

$ open http://localhost:3000

您可以从这里下载本章的完整代码:

github。com/ Apress/ integrating-d3。js-with-react/tree/main/ch03

折线图

我要展示的第一个图表是折线图。折线图以图形方式显示定量数据,被认为是最基本的图表类型之一。折线图由三个绘图元素组成:x 轴、y 轴和一条线。

幸运的是,D3 有一些方法可以帮助你完成创建折线图的整个过程。

line.csv

一个好的起点是数据。对于折线图,我将使用的数据直接来自雅虎融资。我将提取波音股票的历史数据,股票代码为 BA: https://finance.yahoo.com/quote/BA/history 。一旦进入该页面,您将看到一个下载数据的选项。

在下载的 CSV 文件中(见图 3-1 ,我保留了Date列和Open价格,删除了其他列。

img/510438_1_En_3_Fig1_HTML.jpg

图 3-1

BA 历史股票价格的 CSV 文件,在 Microsoft Excel 中打开

接下来,我将把日期重新格式化为%Y-%m-%d的格式,以便于阅读(见图 3-2 )。

img/510438_1_En_3_Fig2_HTML.jpg

图 3-2

包含 BA 历史股票价格的 CSV 文件的格式化日期

我获取 BA 的价格历史,然后将 CSV 转换为两个字段:datavalue

最后,我将把DateOpen列重命名为datevalue

我将文件保存在public/data文件夹中:public/data/line.csv

date,value
2020-01-27,321.75
2020-02-03,318.75
2020-02-10,337.220001
2020-02-17,338.769989
2020-02-24,320
..
..
..

除了数据文件,我还将创建几个文件。

  • BasicLineChart.scss : SCSS 风格文件

  • BasicLineChart.test.tsx:Jest/酵素测试文件

  • BasicLineChart.tsx:组件

  • types.tsts 类型

和往常一样,你可以自己创建这些文件,或者从generate-react-cli那里获得一点帮助。

$ npx generate-react-cli component BasicLineChart --type=d3

types.ts

创建类型文件是保持 TypeScript 类型有组织的常见做法。在我的图表中,我只需要我创建的日期和值,但这是一个很好的习惯,特别是对于复杂的图表,因为需要的类型数量不断增加。在我的例子中,我只需要一种保存日期和值的数据类型。

//  src/component/BasicLineChart/types.ts

export namespace Types {
  export type Data = {
    date: string
    value: number
  }
}

BasicLineChart.tsx

功能组件BasicLineChart做的很重,会画出轴和线图。

我将导入 React,SCSS,D3 和类型文件。结构和我们在上一章中的一样。

我已经将函数组件分解为一个被调用的draw()函数和一个 JSX 占位符。正如我们在上一章所做的一样,draw()函数被useEffect钩子调用。看一看:

// src/component/BasicLineChart/BasicLineChart.tsx

import React, { useEffect } from 'react'
import './BasicLineChart.scss'
import * as d3 from 'd3'
import { Types } from './types'

const BasicLineChart = (props: IBasicLineChartProps) => {
  useEffect(() => {
    draw()
  })

  const draw = () => {

首先,我将设置图表的尺寸和边距。我将通过props从父组件传递这些。

    const width = props.width - props.left - props.right
    const height = props.height - props.top - props.bottom

接下来,我将把一个 SVG 对象添加到我在 JSX 渲染部分设置的包装器div中,并分配属性。我还将添加一个组元素,就像我们在上一章所做的那样。

    const svg = d3
      .select('.basicLineChart')
      .append('svg')
      .attr('width', width + props.left + props.right)
      .attr('height', height + props.top + props.bottom)
      .append('g')
      .attr('transform', `translate(${props.left},${props.top})`)

我不仅可以使用 D3 绘制轴和线,还可以检索 CSV 数据。看一下dsv API ( https://github.com/d3/d3-dsv )。一旦检索到数据,我就将对象转换为我的Types.Data,然后使用d3.timeParse将字符串转换为 D3 Date对象。dsv API 将逐个遍历列表,我将返回一个由解析为d3valueDate组成的对象。

    d3.dsv(',', '/Data/line.csv', (d) => {
      const res = (d as unknown) as Types.Data
      const date = d3.timeParse('%Y-%m-%d')(res.date)
      return {
        date,
        value: res.value,
      }

一旦dsv方法完成,我就用 D3 Date格式的日期和值格式化对象。下一步是添加 x 轴,这将是域的日期。这将刻度的域设置为指定数组的域值,在我们的例子中是日期。

    }).then((data) => {
      const x = d3
        .scaleTime()
        .domain(
          d3.extent(data, (d) => {
            return d.date
          }) as [Date, Date]
        )
        .range([0, width])

Notice

我正在使用d3.extent,并将我的域转换为[Date, Date]d3.extent同时返回最小值和最大值。我还改变了从零到图表宽度的范围。

如果不将d3.extent转换为[Date, Date],我将得到如图 3-3 所示的平均 ESLint 错误消息。从消息中可以看出,它期待一个[Date, Date]

img/510438_1_En_3_Fig3_HTML.jpg

图 3-3

由于不兼容的类型导致的 ESLint 错误消息

TS2345: Argument of type '[undefined, undefined] | [Date, Date]' is not assignable to parameter of type 'Iterable<number | Date | { valueOf(): number; }>'.

最后,我将添加一个组,使用 translate 将该组设置到左下角的位置,并调用d3.axisBottom(x)来附加我的 x 轴。

      svg.append('g').attr('transform', `translate(0, ${height})`).call(d3.axisBottom(x))

Note

call方法是 D3 以选择的形式返回对自身的引用的常用方法。

这里发生的事情是,svg.append('g')将一个 SVG 组元素附加到 SVG 中,并以选择的形式返回对自身的引用。

当我们调用一个选择时,我们是在调用选择g的元素上的函数axisBottom。我们正在新创建和添加的组g上运行axisBottom功能。

对于 y 轴,过程是相似的,只是我设置了一个值而不是日期。我使用数学( https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/max )来获得可能的最大值。

Math.max(...data.map((dt) => ((dt as unknown) as Types.Data).value), 0)

这将确保我的图表设置为基于最大值运行;否则,高度将会不对齐并显示第一个值,而其他值可能会溢出。

      const y = d3
        .scaleLinear()
        .domain([
          0,
          d3.max(data, (d) => {
            return Math.max(...data.map((dt) => ((dt as unknown) as Types.Data).value), 0)
          }),
        ] as number[])
        .range([height, 0])
      svg.append('g').call(d3.axisLeft(y))

对于域,我需要确保对象被强制转换为number[],以避免 ESLint 中又一个不兼容类型的消息。

TS2345: Argument of type '(number | undefined)[]' is not assignable to parameter of type 'Iterable<NumberValue>'. The types returned by '[Symbol.iterator]().next(...)' are incompatible between these types.

最后,我需要添加我想画的线。为此,我可以添加一个 SVG path 元素,并将数据用于我的数据,这样它将遍历我的数据对象来绘制每一行。我正在使用从props开始的填充,笔画宽度为 1.6。

      svg
        .append('path')
        .datum(data)
        .attr('fill', 'none')
        .attr('stroke', props.fill)
        .attr('stroke-width', 1.5)
        .attr(
          'd',
          // @ts-ignore
          d3
            .line()
            .x((d) => {
              return x(((d as unknown) as { date: number }).date)
            })
            .y((d) => {
              return y(((d as unknown) as Types.Data).value)
            })
        )
    })
  }

在渲染方面,我设置了一个包装器div,其className值为basicLineChart

  return <div className="basicLineChart" />
}

对于接口,我将从父组件传递属性,这样我就可以对齐组件并设置填充颜色,这样就可以很容易地重用我的组件。

interface IBasicLineChartProps {
  width: number
  height: number
  top: number
  right: number
  bottom: number
  left: number
  fill: string
}

export default BasicLineChart

请注意,这些 D3 比例方法真的帮了大忙;他们能够进行计算,并将我们的数据转换成绘制图表所需的值。

如果我需要在基本折线图上添加一个脉动点或任何东西,我能做的就是重用这些 x 轴和 y 轴的值,例如y(300)

这个什么时候派上用场?假设我想在图表的末尾再画一条线。我可以存储 x,y 的最后位置,然后用 y 轴和 x 轴计算我想要的任何价格。

svg
 .append('line')
 .style('stroke', 'red')
 .style('stroke-width', 1)
 .attr('x1', lastX)
 .attr('y1', lastY)
 .attr('x2', lastX + x2)
 // y Axis is what turn the value to the value needed
 // on the chart
 .attr('y2', yAxis(300))

basiclinechart . scss

至于我的 SCSS 文件,我可以在这里设置我的绘图元素的属性,但我真的不需要设置任何东西,因为我正在使用props传递我的填充颜色和其他属性。也就是说,准备好我的 SCSS 供将来使用是一个好习惯。

.basicLineChart {
}

basiclenechart . test . tsx

为了测试,我使用 Jest 和 Enzyme 来确保组件挂载并使用我设置的props

如果你是第一次尝试用笑话和酶 React,看看我在 https://medium.com/react-courses/unit-testing-react-typescript-app-with-jest-jest-dom-enzyme-11f52487aa18 的文章。更多详情,请阅读我的《React 进度书: https://www.apress.com/gp/book/9781484266953

// src/component/BasicLineChart/BasicLineChart.test.tsx

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

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

  beforeEach(() => {
    component = shallow(<BasicLineChart top={10} right={50} bottom={50} left={50} width={460} height={400} fill="tomato" />)
  })

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

App.tsx

最后,我可以用我设置的props添加简单的BasicLineChart

// src/App.tsx

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

import BasicBarChart from './components/BasicBarChart/BasicBarChart'

function App() {
  return (
    <div className="App">
      <header className="App-header">
        <BasicBarChart top={10} right={50} bottom={50} left={50} width={900} height={400} fill="tomato" />
      </header>
    </div>
  )
}

export default App

最终结果见图 3-4 。

img/510438_1_En_3_Fig4_HTML.jpg

图 3-4

英航股价折线图

现在我已经准备好了我的图表,我将运行我在package.json运行脚本中设置的格式、lint 和测试任务,以确保质量。

$ yarn format
$ yarn lint
$ yarn test

如果你打开package.json并查看scripts标签下,你可以看到这些任务被设置在那里。

"scripts": {
  "format": "prettier --write 'src/**/*.{ts,tsx,scss,css,json}'",
  "lint": "eslint --ext .js,.jsx,.ts,.tsx ./",
  "test": "react-scripts test",
  ..
  ..
}

我不会详细讨论格式、lint 和测试是如何设置的,但是正如我之前指出的,你可以在我的 React 和 Libraries 的书中了解更多,可以在 https://www.apress.com/gp/book/9781484266953 找到。

如果你不设置测试套件,这很好,不会影响任何功能;然而,用完整的测试覆盖来编写你的组件只是一个好的实践。

对比图

面积图以图形方式显示定量数据,类似于折线图。

不同的是轴和线之间的区域用颜色强调。

在编码方面,类似于我们刚刚在上例中做的折线图;唯一的区别是这个区域是有颜色的

area.csv

至于数据,我将使用标准普尔 500 股票行情自动收报机,并将数据格式化,就像我在折线图中所做的那样( https://finance.yahoo.com/quote/%5EGSPC/history )。

我将在public/data/area.csv保存结果。

date,value
2020-01-27,3282.330078
2020-02-03,3235.659912
2020-02-10,3318.280029
..
..

BasicAreaChart.tsx

我的主成分BasicAreaChart.tsx,和BasicLineChart.tsx差不多。看一看:

// src/component/BasicAreaChart/BasicAreaChart.tsx

import React, { useEffect } from 'react'
import './BasicAreaChart.scss'
import * as d3 from 'd3'
import { Types } from './types'

const BasicAreaChart = (props: IBasicAreaChartProps) => {
  useEffect(() => {
    draw()
  })

  const draw = () => {

我设置尺寸和边距。

    const width = props.width - props.left - props.right
    const height = props.height - props.top - props.bottom

接下来,我将svg对象添加到我将在渲染端设置的basicAreaChart JSX div中。

    const svg = d3
      .select('.basicAreaChart')
      .append('svg')
      .attr('width', width + props.left + props.right)
      .attr('height', height + props.top + props.bottom)
      .append('g')
      .attr('transform', `translate(${props.left},${props.top})`)

    d3.dsv(',', '/Data/area.csv', (d) => {
      const res = (d as unknown) as Types.data
      const date = d3.timeParse('%Y-%m-%d')(res.date)
      return {
        date,
        value: res.value,
      }
    }).then(function results(data) {

现在我可以将日期格式设置为 x 轴。

      const x = d3
        .scaleTime()
        .domain(
          d3.extent(data, (d) => {
            return d.date
          }) as [Date, Date]
        )
        .range([0, width])

      svg.append('g').attr('transform', `translate(0, ${height})`).call(d3.axisBottom(x))

我也可以将 y 轴设置为值。

      const y = d3
        .scaleLinear()
        // @ts-ignore
        .domain([
          0,
          d3.max(data, (d) => {
            return +d.value
          }),
        ] as number[])
        .range([height, 0])
      svg.append('g').call(d3.axisLeft(y))

对于图表的线条,我将使用路径,就像在BasicLineChart.tsx中一样。

      svg
        .append('path')
        .datum(data)
        .attr('fill', props.fill)
        .attr('stroke', 'white')
        .attr('stroke-width', 1.5)

面积图最大的不同是,我现在使用的是 D3 面积和曲线 API。

d3
.area()
.curve(d3.curveLinear)

这段代码是对路径线的补充,因为我需要用颜色填充这个区域。

我们通过设置xy0y1值来实现。

* @param x Sets the x accessor - in our case a date
* @param y0 Sets the y0 accessor - in our case it's zero since we start from the bottom.
* @param y1 Sets the y1 accessor - in our case it's the value of the stock.

看一看:

        .attr(
          'd',
          // @ts-ignore
          d3
            .area()
            .curve(d3.curveLinear)
            .x((d) => {
              return x(((d as unknown) as { date: number }).date)
            })
            .y0(y(0))
            .y1((d) => {
              return y(((d as unknown) as Types.data).value)
            })
        )
    })
  }

在渲染方面,我添加了div包装器。

  return <div className="basicAreaChart" />
}

界面与折线图相同。

interface IBasicAreaChartProps {
  width: number
  height: number
  top: number
  right: number
  bottom: number
  left: number
  fill: string
}

export default BasicAreaChart

接下来,设置types.tsscssBasicAreaChart.test.tsx文件。它们的代码与 line 示例中的代码相同,所以我不会在这里向您展示它们。确保它们被设置在BasicAreaChart文件夹中(参见图 3-5 )。

img/510438_1_En_3_Fig5_HTML.jpg

图 3-5

基础图表文件结构

App.tsx

最后,我们需要设置App.tsx来包含我们的BasicAreaChart,并通过props

// src/App.tsx

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

import BasicAreaChart from './components/BasicAreaChart/BasicAreaChart'

function App() {
  return (
    <div className="App">
      <header className="App-header">
        <BasicAreaChart top={10} right={50} bottom={50} left={50} width={1000} height={400} fill="tomato" />
      </header>
    </div>
  )
}

export default App

然后瞧啊!见图 3-6 。

img/510438_1_En_3_Fig6_HTML.jpg

图 3-6

基本图表完成

如你所见,折线图和面积图是相似的。一旦我们理解了检索和格式化数据、创建 x 轴和 y 轴以及绘制图表的过程,每次绘制图表都会变得更加容易。

条形图

我将在本章中创建的最后一个图表是另一种常用的图表类型,条形图。

条形图用于显示和比较数字、频率或其他指标。条形图之所以受欢迎,是因为它的创建非常简单,并且易于解释。

我已经在第二章向你展示了如何创建一个简单的条形图;然而,条形图没有 x,y 轴,并且数据不是从外部文件加载的。

在这个例子中,对于数据,我将使用 Stack Overflow 调查数据来显示 React 和其他框架的受欢迎程度: https://insights.stackoverflow.com/survey/2020 。这个图表帮助你选择一个网络框架。

酒吧. csv

对于这些数据,我用从栈溢出调查中复制的值创建了一个名为public/data/bar.csv的 CSV 文件。看一看:

framework,value
jQuery,43.3
React.js,35.9
Angular,25.1
ASP.NET,21.9
Express,21.2
.NET Core,19.1
..
..

BasicBarChart.tsx

在结构方面,我将保持类似于我之前创建的折线图和面积图的结构。

// src/component/BasicBarChart/BasicBarChart.tsx

import React, { useEffect } from 'react'
import './BasicBarChart.scss'
import * as d3 from 'd3'
import { Types } from './types'

const BasicBarChart = (props: IBasicBarChartProps) => {
  useEffect(() => {
    draw()
  })

  const draw = () => {

像以前一样,我们设置尺寸和边距。

    const width = props.width - props.left - props.right
    const height = props.height - props.top - props.bottom

为了绘制 x,y 范围,我将从 D3 获得一些帮助,并基于从父组件通过props传递的属性来设置它们。

    const x = d3.scaleBand().range([0, width]).padding(0.1)
    const y = d3.scaleLinear().range([height, 0])

接下来,我将把 SVG 对象追加到我的名为basicBarChartdiv包装器中,我将在渲染时添加该包装器,我将添加一个组并设置我的 SVG 宽度和高度属性。

    const svg = d3
      .select('.basicBarChart')
      .append('svg')
      .attr('width', width + props.left + props.right)
      .attr('height', height + props.top + props.bottom)
      .append('g')
      .attr('transform', `translate(${props.left},${props.top})`)

    d3.dsv(',', '/Data/bar.csv', (d) => {
      return (d as unknown) as Types.Data

一旦数据对象准备好了,我就可以在域中缩放Data的范围。

    }).then((data) => {
      x.domain(
        data.map((d) => {
          return d.framework
        })
      )
      y.domain([
        0,
        d3.max(data, (d) => {

我将使用我在内嵌图表中使用的相同数学函数来设置 y 的max值,并将我的域转换为number[]以避免 ESLint 对我咆哮。

          return Math.max(...data.map((dt) => (dt as Types.Data).value), 0)
        }),
      ] as number[])

为了绘制实际的条形图,我将使用selectAlldata属性,这样 D3 将遍历我的数据并为条形图添加矩形。

      svg
        .selectAll('.bar')
        .data(data)
        .enter()
        .append('rect')
        .attr('fill', props.fill)
        .attr('class', 'bar')
        .attr('x', (d) => {
          return x(d.framework) || 0
        })

注意,在返回时,我使用了“或零”:|| 0。原因是我们不确定是否有值,数据可以是未定义的(number | undefined)。这就是 TS 需要那个“或零”的原因——这是为了避免得到 ESLint 过载错误消息。

TS2769: No overload matches this call. Overload 1 of 4, '(name: string, value: null): Selection<SVGRectElement, Data, SVGGElement, unknown>', gave the following error. Argument of type '(this: SVGRectElement, d: Data) => number | undefined' is not assignable to parameter of type 'null'.

对于宽度,我使用的是x. bandwidth,它返回构成条形图的每个 bin(矩形)的宽度。对于高度→这将是图表边界的高度减去创建容器高度值的值。

        .attr('width', x.bandwidth())
        .attr('y', (d) => {
          return y(d.value)
        })
        .attr('height', (d) => {
          return height - y(d.value)
        })

接下来,我将添加 x 轴和 y 轴。

      svg.append('g').attr('transform', `translate(0,${height})`).call(d3.axisBottom(x))

      svg.append('g').call(d3.axisLeft(y))
    })
  }

现在我呈现我的名为basicBarChartdiv包装器。

  return <div className="basicBarChart" />
}

最后,我设置了我的接口。

interface IBasicBarChartProps {
  width: number
  height: number
  top: number
  right: number
  bottom: number
  left: number
  fill: string
}

export default BasicBarChart

正如我们在其他例子中所做的那样,设置types.tsscssBasicAreaChart.test.tsx

types.ts

对于类型,我设置了两个变量:frameworkvalue(类型为stringnumber)。

// src/component/BasicBarChart/types.ts

export namespace Types {
  export type Data = {
    framework: string
    value: number
  }
}

basicbarchart . scss

对于 SCSS,我可以在那里设置每个条的填充,但是由于我是在props中设置的,这将是一个重叠。我只是想告诉你,如果你需要的话,为我们在 SCSS 创建的 D3 元素设置属性不仅仅是可以接受的,而且很容易阅读和修改,尤其是当你在一个有设计师的团队中工作的时候。

.basicBarChart {
}

.bar {
  fill: tomato;
}

App.tsx

至于App.tsx,你已经知道该怎么做了,所以继续添加组件吧。

// src/App.tsx

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

import BasicBarChart from './components/BasicBarChart/BasicBarChart'

function App() {
  return (
    <div className="App">
      <header className="App-header">
        <BasicBarChart top={10} right={50} bottom={50} left={50} width={900} height={400} fill="tomato" />
      </header>
    </div>
  )
}

export default App

又来了!见图 3-7 。

img/510438_1_En_3_Fig7_HTML.jpg

图 3-7

条形图最终结果

看图表,你可以看到图表讲述了一个故事。乍一看,2020 栈溢出结果似乎表明 React 绕过了 Angular 近 11%。

从图表中可以看出(图 3-8 ),jQuery 显示为王者,但 React.js 正在蓄势待发,准备接管。

img/510438_1_En_3_Fig8_HTML.jpg

图 3-8

https://insights.stackoverflow.com/survey/2020#community

然而,事实并非如此。

该调查分别包括 React.js 和 Gatsby,尽管它们都基于 React,Angular 和 Angular.js 之间也有分裂。

实际上,如果我们把这些结果加在一起,Angular 的 41.2%、React 的 39.9%和 jQuery 的 43.3%几乎是完全相等的。

真实的结果是 React、Angular 和 jQuery 之间更加平均。如果我要相应地调整我的数据文件,我会得到这样的结果:

framework,value
jQuery,43.3
Angular + Angular.js,41.2
React.js + Gatsby,39.9

一旦我输入新的数据,我将得到一个完全不同的故事;参见图 3-9 。

img/510438_1_En_3_Fig9_HTML.jpg

图 3-9

react vs Angular vs jQuery 2020

如果你有兴趣比较 React 和 Angular,可以看看我在 https://medium.com/react-courses/angular-9-vs-react-16-a-2020-showdown-2b0b8aa6c8e9 发表的关于媒体的文章。

在写这本书的时候,2021 栈溢出的结果还没有发表,但是看看这些值如何随着时间的推移而变化会很有趣。现在你已经有了这张图表,你可以插入新的数据了。

现在我已经准备好了所有三个图表,我将最后一次运行 format、lint 和 test 任务,以确保质量。

$ yarn format
$ yarn lint
$ yarn test

继续将您的结果与我的进行比较(参见图 3-10 )。

img/510438_1_En_3_Fig10_HTML.jpg

图 3-10

所有图表的测试套件结果通过

Done in 1.48s.$ eslint --ext .js,.jsx,.ts,.tsx ./
✨  Done in 9.88s.Test Suites: 5 passed, 5 total
Tests: 5 passed, 5 total
Snapshots: 0 total

摘要

在这一章中,我向你展示了如何用 React、ts 和 D3 创建流行的和基本的图表。我们创建了以下三种类型的图表:

  • 折线图

  • 对比图

  • 条形图

我向您展示了如何最大限度地利用 D3,不仅用于绘图,甚至用于检索数据,我还向您展示了如何避免常见的 ESLint 错误消息,因为 TS 要求拥有类型。我也给了你一些技巧,关于如何组织你的作品,用格式、lint 和测试运行脚本进行质量检查。

查看我的 d3 和 React 交互课程,看看你可以用不同的方法实现本章中的所有例子。互动课程涵盖了本节的更多主题,例如,对 DOM、色彩空间、交互性、设计的更多控制,以及对本章内容的扩展。该课程灵活地补充了本章和本书;https://elielrom.com/BuildSiteCourse

下一章将继续创建基本图表,我们将创建另外三个基本图表。

  • 圆形分格统计图表

  • 散点图

  • 直方图

四、基本图表:第二部分

正如您已经看到的,D3 是创建图表的标准,所以如果您对创建和定制图表很认真,您就无法逃避对 D3 的学习。React 与其他库(如 D3)集成;但是,将 TypeScript 添加到组合中确实需要特别注意。

在前面的章节中,我向您展示了如何使用 React、ts 和 D3 创建流行和基本类型的图表。此外,如果你上过我的 React + d3 交互课程( https://elielrom.com/BuildSiteCourse ),你会看到你可以用不同的方法实现上一章中的所有例子,比如应用记忆回调、处理大小调整、更多的交互以及处理 DOM。与只使用 JS 相比,React + d3 + TS 的组合需要一些特别的注意,本章和上一章的基本图表反映了我发现最有效的东西。

在这一章中,我将介绍如何使用 TypeScript 作为类型检查器,用 React 和 D3 创建更简单的图表。

我将向您展示如何创建以下简单的图表,重点是让 D3 完成大部分工作:

  • 圆形分格统计图表

  • 散点图

  • 直方图

我们开始吧。

圆形分格统计图表

饼图是最基本和最流行的图表类型之一。图表类型是圆形的统计图形。饼图通过使用切片表示整体的比例来表示数字。

英尺. csv

我的图表的数据指标只是总计 100%的随机数。

name,value
a,25
b,3
c,45
d,7
e,20

除了数据文件之外,我还将创建几个文件,就像我在上一章所做的那样。

  • BasicPieChart.tsx:主要成分

  • BasicPieChart.test.tsx : Jest 和酵素测试

  • SCSS 前置处理器

  • 保存我将要使用的类型的文件

和第一部分一样,您可以自己创建这些文件,或者从generate-react-cli那里获得一些帮助。

$ npx generate-react-cli component BasicPieChart --type=d3

types.ts

我的类型由与数据文件相同的列组成,如namevalue

// src/component/BasicPieChart/types.tsexport namespace Types {
  export type Data = {
    name: string
    value: number
  }
}

BasicPieChart.tsx

对于BasicPieChart,流程类似于我们在第一部分搭建的图表,使用useEffect绘制方法,加载数据,绘制图表。看一看:

// src/component/BasicPieChart/BasicPieChart.tsx

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

D3 是模块化构建的,所以我需要PieArcDatum ( https://github.com/d3/d3-shape )。PieArcDatum泛型是指传递给Pie生成器的输入数组中元素的数据类型。我将使用PieArcDatum来更好地投射我的物体。确保添加模块,如下所示:

yarn add d3-shape

看一看:

import { PieArcDatum } from 'd3-shape'
import { Types } from './types'

const BasicPieChart = (props: IBasicPieChartProps) => {
  useEffect(() => {
    draw()
  })

对于draw()方法,我将设置饼图的宽度、高度和半径。

  const draw = () => {
    const width = props.width - props.left - props.right
    const height = props.height - props.top - props.bottom
    const radius = Math.min(width, height) / 2

接下来,我选择我将要渲染的basicPieChart div,并添加一个名为svg的组,带有一个transform属性。

    const svg = d3
      .select('.basicPieChart')
      .append('svg')
      .attr('width', width)
      .attr('height', height)
      .append('g')
      .attr('transform', `translate(${width / 2},${height / 2})`)

我将上传 CSV pie.csv数据文件。

    d3.dsv(',', '/Data/pie.csv', (d) => {
      const res = (d as unknown) as Types.Data
      return {
        name: res.name,
        value: res.value,
      }
    }).then((data) => {

一旦我的数据对象准备好了,下一步就是设置色标。我将使用d3.scaleOrdinal(),对于域名,我可以设置每个名称有自己独特的颜色。

D3 使事情变得简单,因为有一组预定义的分类配色方案,所以我可以使用d3.schemeCategory10或任何其他( https://github.com/d3/d3-scale-chromatic )颜色类别。

const color = d3
        .scaleOrdinal()
        .domain(
          (d3.extent(data, (d) => {
            return d.name
          }) as unknown) as string
        )
        .range(d3.schemeCategory10)

注意,虽然我使用的是d3.schemeCategory10,但是我可以创建自己的配色方案,它可以作为props传递或者在我的数据文件中定义。

.range(['#000000', '#000000', '#000000', '#000000', '#000000'])

下一步是遍历我的数据并创建饼图。我可以把我的数据转换成键值对,然后把它传递给一个路径元素,就像这样:

const map = d3.map(data, (d) => {
  return { 'key': d.name, value: d.value }
})

但是有更好的方法。我可以用我的数据类型设置饼图,用我的数据类型使用泛型PieArcDatum,并为半径生成路径。然后我插入我的数据来创建一个饼图数据,我可以用它来遍历结果。

      const pie = d3
        .pie<Types.Data>()
        .sort(null)
        .value((record) => record.value)

      const path = d3.arc<PieArcDatum<Types.Data>>().innerRadius(0).outerRadius(radius)

      const pieData = pie(data)

现在我需要做的就是为每个饼图数据生成 arch SVGs,并使用我为每个名称创建的颜色。

      const arch = svg
        .selectAll('.arc')
        .data(pieData)
        .enter()
        .append('g')
        .attr('class', 'arc')
        .attr('fill', (d) => {
          return color(d.data.name) as string
        })

      arch.append('path').attr('d', path)
    })
  }

渲染方面,我需要一个包装div

  return <div className="basicPieChart" />
}

对于我的props接口,我正在放置将从我的父组件传递的对齐元素。

interface IBasicPieChartProps {
  width: number
  height: number
  top: number
  right: number
  bottom: number
  left: number
}

export default BasicPieChart

BasicPieChart.test.tsx

对于测试,我使用 Jest 和 Enzyme 来确保组件挂载,并使用我设置的props

// src/component/BasicPieChart/BasicPieChart.test.tsx

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

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

  beforeEach(() => {
    component = shallow(<BasicPieChart width={900} height={400} top={10} right={50} bottom={50} left={50} />)
  })

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

App.tsx

最后,我的父组件App.tsx需要包含我的BasicPieChart和对齐props

// src/App.tsx

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

import BasicPieChart from './components/BasicPieChart/BasicPieChart'

function App() {
  return (
    <div className="App">
      <header className="App-header">
        <BasicPieChart width={400} height={400} top={10} right={10} bottom={10} left={10} />
      </header>
    </div>
  )
}

export default App

basicpiechart . scss

对于 SCSS 文件,我定义了一个占位符。我还不需要任何 SCSS,但是创建一个 SCSS 文件是一个很好的实践。

.basicPieChart {
}

再看一下本地主机端口 3000: http://localhost:3000/。你可以将你的结果与我的进行比较,如图 4-1 所示。

img/510438_1_En_4_Fig1_HTML.jpg

图 4-1

React 和 D3 饼图

和往常一样, y 你可以从这里下载本章的完整代码:

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

要查看这个与 React 更好集成的基本饼图,以及调整大小和切换指标的交互,请查看我的 React + d3 交互课程:https://elielrom.com/BuildSiteCourse

BasicDonutChart.tsx

要创建一个小圆环图,过程几乎是相同的。我将使用PieArcDatum为内圆和外圆绘制圆弧,以创建一个圆环。变化很简单。当我们创建弧线时,只需更新'innerRadius'属性;innerRadius(10)

在下面的例子中,我可以修改代码并使用props来传递来自父组件的数据,而不是将它加载到我的组件中。我还将编写代码,以便在需要时可以使用该组件来更改数据。让我们来看看。

首先使用yarn add d3-shape导入库。

看一看BasicDonutChart.tsx代码:

import React, { RefObject, useEffect, useState } from 'react'
import * as d3 from 'd3'
import { PieArcDatum } from 'd3-shape'
import { Types } from './types'

const BasicDonutChart = (props: IBasicDonutChartProps) => {
 const ref: RefObject<HTMLDivElement> = React.createRef()
 const [data, setData] = useState<Types.Data[]>([])

useEffect里面,我会检查数据。这是必要的,以确保我只在新数据被更新时才改变饼图。例如,当你有新数据时,这种情况就会发生。

我所做的是将数据存储在一个 React 状态对象上,然后使用JSON.stringify将状态数据与props数据进行比较,看看是否有变化。如果有变化,我会将新数据存储在状态中。

 useEffect(() => {
   if (JSON.stringify(props.data) !== JSON.stringify(data)) {
     setData(props.data)

     const { width } = props
     const { height } = props

     const svg = d3
       .select(ref.current)
       .append('svg')
       .attr('width', width)
       .attr('height', height)
       .append('g')
       .attr('transform', `translate(${width / 2}, ${height / 2.5})`)

     const color = ['#068606', '#C1C0C0']

     const donut = d3
       .pie<Types.Data>()
       .sort(null)
       .value((record) => record.value)

     const path = d3.arc<PieArcDatum<Types.Data>>().innerRadius(10).outerRadius(20)

     const donutData = donut(props.data)

     const arch = svg
       .selectAll('.arc')
       .data(donutData)
       .enter()
       .append('g')
       .attr('class', 'arc')
       .attr('fill', (d, i) => {
         return color[i] as string
       })

     arch.append('path').attr('d', path)
   }
 }, [data, props, props.data, props.height, props.width, ref])

我需要指定我在useEffect中使用的变量。

 return <div className="basicDonutChart" ref={ref} />
}

interface IBasicDonutChartProps {
 data: Types.Data[]
 width: number
 height: number
}

export default BasicDonutChart

注意,在我的例子中,我使用了一个引用,而不是 D3 select。这样,我就可以将我的 pie 作为列表项呈现器,以防我需要在列表中使用这个组件。

App.tsx

要实现这一点,您可以在父组件中设置图表,并在App.tsx中传递数据。

<BasicDonutChart
 data={[
   { name: 'Yes', value: 80 },
   { name: 'No', value: 20 },
 ]}
 width={50}
 height={50}
/>

看一下图 4-2 。

img/510438_1_En_4_Fig2_HTML.jpg

图 4-2

基本圆环饼图

至于数据,在前面的例子中,我向您展示了实际的图表组件如何检索数据。

这使得我们的代码易于阅读和松散耦合,这是一个保持图表简单和数据在一个地方的伟大设计;然而,为了让您为下一章处理状态管理做好准备,这里我将数据提取到父组件App.tsx

我们希望从图表组件中提取数据的原因是为了在多个组件之间共享数据。在这种情况下,我们希望一次性加载数据,并与多个组件共享。一个很好的例子是使用相同的数据绘制不同类型的图表。

散点图

散点图(又名散点图散点图 h)用点表示数值。散点图是观察变量之间关系的好方法。

散点. csv

使用散点图的一个有趣方法是观察钻石价格与钻石大小的关系。我在 GitHub ( https://github.com/sakshi296/P1-1-Predicting-Diamond-Prices )上找到了发布的数据。一旦我下载了图表,我可以在 Excel 或任何其他程序中打开它来修改它,如图 4-3 所示。

img/510438_1_En_4_Fig3_HTML.jpg

图 4-3

每克拉钻石价格 CSV 数据

我将删除所有不需要的列,保留价格和克拉指标(见图 4-4 )。

img/510438_1_En_4_Fig4_HTML.jpg

图 4-4

清洗后每克拉钻石价格

我们的数据集很小,占用空间很小,但是清理您的数据并设置您的数据集以仅使用您需要的数据是优化您的数据并提高性能的良好做法。在第十章中,我将深入探讨优化图表的最佳实践。

我将把我的文件保存为public/data/scatter.csv中的scatter.csv

price,carat
1749,0.51
7069,2.25
2757,0.7
1243,0.47
789,0.3
728,0.33
...
...

types.ts

对于我的类型脚本数据,我将设置与我的 CSV 列相同的名称:pricecarat度量。

// src/component/BasicScatterChart/types.ts

export namespace Types {
  export type Data = {
    price: number
    carat: number
  }
}

BasicScatterChart.tsx

现在我准备开始绘制我的图表。

// src/component/BasicScatterChart/BasicScatterChart.tsx

import React, { useEffect } from 'react'
import './BasicScatterChart.scss'
import * as d3 from 'd3'
import { Types } from './types'

const BasicScatterChart = (props: IBasicScatterChartProps) => {
  useEffect(() => {
    draw()
  })  const draw = () => {
    const width = props.width - props.left - props.right
    const height = props.height - props.top - props.bottom

    const svg = d3
      .select('.basicScatterChart')
      .append('svg')
      .attr('width', width + props.left + props.right)
      .attr('height', height + props.top + props.bottom)
      .append('g')
      .attr('transform', `translate(${props.left},${props.top})`)

    d3.dsv(',', '/Data/diamonds.csv', (d) => {
      return {
        price: d.price,
        carat: d.carat,
      }
    }).then((data) => {

一旦数据准备就绪,我将创建 x 轴和 y 轴外设。第一步是找出价格和克拉的最高值,然后我可以将其设置为我的 axis max 值。

const maxPrice = Math.max(...data.map((dt) => (dt as unknown as Types.Data).price), 0)
const maxCarat = Math.max(...data.map((dt) => (dt as unknown as Types.Data).carat), 0)

接下来,我可以使用d3.scaleLinear来设置我的 x 轴和 y 轴。

      const x = d3.scaleLinear().domain([0, 18000]).range([0, width])
      svg.append('g').attr('transform', `translate(0,${height})`).call(d3.axisBottom(x))

      const y = d3.scaleLinear().domain([0, 4.5]).range([height, 0])
      svg.append('g').call(d3.axisLeft(y))

最后一部分是使用一个 circle SVG 元素绘制圆点,这个元素带有一种填充颜色,我将从父组件传递过来。我将我的半径设置为 1px,因为我有这么多的结果,但是你可以用更小的结果来尝试。

      svg
        .append('g')
        .selectAll('dot')
        .data(data)
        .enter()
        .append('circle')
        .attr('cx', (d) => {
          return x(((d as unknown) as Types.Data).price)
        })
        .attr('cy', (d) => {
          return y(((d as unknown) as Types.Data).carat)
        })
        .attr('r', 0.8)
        .style('fill', props.fill)
    })
  }

  return <div className="basicScatterChart" />
}

interface IBasicScatterChartProps {
  width: number
  height: number
  top: number
  right: number
  bottom: number
  left: number
  fill: string
}

export default BasicScatterChart

App.tsx

BasicScatterChart组件添加到我的App.tsx中。

// src/App.tsx

import BasicScatterChart from './components/BasicScatterChart/BasicScatterChart'

<BasicScatterChart width={800} height={400} top={10} right={50} bottom={50} left={50} fill="tomato" />

最后,如果您之前没有这样做,请创建BasicScatterChart.scssBasicScatterChart.test.tsx

  • 这只是 SCSS 的一个占位符。

  • BasicScatterChart.test.tsx:这个跟BasicPieChart.test.tsx一样。

现在,我们看到了钻石每克拉的历史价格,如图 4-5 所示。

img/510438_1_En_4_Fig5_HTML.jpg

图 4-5

React 和 D3 散点图

这张图表可以让我一目了然地看到价格范围,我可以看到每颗钻石的克拉大小和价格。如果我想改进图表,我可以插入其他字段,如钻石颜色的等级度量,并在图表上给出这些不同的颜色。我可以每年改变图表并过滤数据。

现在我已经准备好了所有三个图表,我将最后一次运行 format、lint 和 test 任务,以确保质量。

$ yarn format
$ yarn lint
$ yarn test

把你的结果和我的比较一下。

Done in 1.97s.
$ yarn lint
yarn run v1.22.10
$ eslint — ext .js,.jsx,.ts,.tsx ./
✨ Done in 10.14s.
Test Suites: 7 passed, 7 total
Tests: 7 passed, 7 total
Snapshots: 0 total
Time: 9.476s

参见图 4-6 。

img/510438_1_En_4_Fig6_HTML.jpg

图 4-6

基本图表组件的测试结果

在“我的 d3 和 react 交互”课程中,你将学习如何包含交互方式线和设置调整大小,以及设置组件以便 React 更好地控制 DOM,参见 https://elielrom.com/BuildSiteCourse

设置在鼠标移动事件时移动的交互式平均线可以帮助用户更好地阅读结果,并快速预测每克拉的价格。

直方图

到目前为止,我们创建的图表相对简单。

在环形饼图中,我们确实检查了数据是否发生了变化,并且我们在函数组件状态中存储了新的数据集,但是我们没有在图表中实现任何变化。

我的目的只是向您展示如何将 D3 集成到 React 组件中,该组件使用 TypeScript 作为类型检查器,并尽可能多地使用 D3。

本章中我们将创建的最后一个图表是一个基本的直方图,它将包括来自用户、交互和动画的输入。这次我们将使用类组件,因此您可以看到如何使用 React 类组件内置的挂钩将动画和更改联系在一起。

直方图是由价格和时间指标组成的条形图。直方图是显示值落入范围的频率的常用方法。

直方图将数字数据分组到多个条块中。那么容器可以显示为分段的列。

我们将建立一个图表,回顾以太币的价格随时间的变化,这样你就能以一种简单直观的方式看到硬币的销售价格。

types.ts

对于 TypeScript 类型,我创建了两种类型。第一种类型用于数据(Data),该对象保存硬币的价格。我将使用的第二种类型(BarsNode)是处理条形,以及重新绘制这些条形。看一看:

//src/component/Histogram/types.ts

export namespace Types {
  export type Data = {
    price: number
  }
  export type BarsNode = {
    x0: number
    x1: number
    length: number
  }
}

接下来,对于实际的直方图组件,我将再次创建三个文件。这里没有什么新东西:

  • Histogram.tsx:自定义组件

  • Histogram.scss:风格

  • Histogram.test.tsx有一个测试

直方图. tsx

在直方图组件中,我将设置一个滑块。用户输入将决定显示多少条。一旦用户使用滑块输入选择,我就可以重新绘制图表。对于滑块,我将使用 Material-UI slide 组件,所以除了所有常用的导入之外,让我们导入滑块。此外,我将使用 Material-UI 中的排版模块来绘制我的图表。

// src/component/Histogram/Histogram.tsx

import React from 'react'
import './Histogram.scss'
import * as d3 from 'd3'
import Slider from '@material-ui/core/Slider'
import { Typography } from '@material-ui/core'
import { Types } from './types'

对于类签名,我将使用类纯组件(React.PureComponent)而不是React.Component,因为我不需要使用shouldComponentUpdate事件生命周期。

我的props和状态props接口将包含调整图表的属性。

export default class Histogram extends React.PureComponent<IHistogramProps, IHistogramState> {
  constructor(props: IHistogramProps) {
    super(props)

我的状态将由用户想要画多少刻度(条)组成;起始状态是 10。

    this.state = {
      numberOfTicks: 10,
    }
  }

接下来,一旦用户对滑块进行了更改,我们需要重新绘制图表。为此,我们使用了 Material-UI slider change 事件;然而,由于 React 虚拟 DOM 的工作方式,这并不保证我们的图表会得到更新。最好的方法是除了在初始渲染时调用的componentDidMount之外,还使用componentDidUpdate

  componentDidMount() {
    this.draw()
  }

  componentDidUpdate(prevProps: IHistogramProps, prevState: IHistogramState) {
    this.draw()
  }

现在我们也可以使用getDerivedStateFromProps代替componentDidUpdate,但是这个方法可能会在一次更新中被调用多次,所以我们需要放置一个验证器来检查状态是否被更新。

在更新发生后立即被调用。初始呈现时不调用此方法。

避免任何副作用是很重要的,所以您应该使用componentDidUpdate,它只在组件更新后执行一次。

一旦滑块改变,我们需要用我们想要显示的刻度数的新值来更新我们的状态,这发生在handleChange方法中。事件的类型为React.ChangeEvent。我还可以传递作为更新结果的新值。

  handleChange = (event: React.ChangeEvent<{}>, newValue: number | number[]) => {

一旦该事件被调用,我就可以将状态设置为numberOfTicks。我将绑定numberOfTicks,因此更新将会发生。

    const value = newValue as number
    this.setState((prevState: IHistogramState) => {
      return {
        ...prevState,
        numberOfTicks: value,
      }
    })
  }

重物的提升是用拉的方法完成的。我可以将这段代码更多地分解成一个助手类,但是这个例子并不太复杂。

我使用d3.selectAllhistogramChart设置为包装元素。

  draw = () => {
    const histogramChart = d3.selectAll('.histogramChart')

接下来,我将从图表中清除 x 和 y,因为它们可能会改变。这在第一次绘制时不需要,但在重新绘制时需要。为此,我使用了removeremove将删除我的主包装器下的所有组元素。

   d3.selectAll('.histogramChart').selectAll('g').remove()

一旦移除了这些条,我将为 x 轴和 y 轴创建一个新的 group SVG 元素,并将其添加到histogramChart group 元素中。

    const xAxisGroupNode = histogramChart.append('g')
    const yAxisGroupNode = histogramChart.append('g')

接下来,让我们初始化并缩放 x 轴。我在烘烤的价值,但他们可以动态设置。

    const xAxis = d3.scaleLinear().domain([75, 650]).range([0, this.props.width])

然后,我可以画出 x 轴。

    xAxisGroupNode.attr('transform', `translate(0,${this.props.height})`).call(d3.axisBottom(xAxis))

y 轴也是一样:初始化,缩放,然后绘制。

    const yAxis = d3.scaleLinear().range([this.props.height, 0])

我可以利用d3.bin ( https://github.com/d3/d3-array ),将数据点分组到桶中。我们可以为直方图设置数据、域和参数。我的领域数据在 0-750 之间,所以我正在烘烤它。

    const histogram = d3
      .bin()
      .value((d) => {
        return ((d as unknown) as Types.Data).price
      })
      .domain([0, 750])
      .thresholds(xAxis.ticks(this.state.numberOfTicks))

接下来,将此函数应用于数据以获得箱:

    const bins = histogram(this.props.data as Array<never>)

一旦我们设置了域并绘制了图表,y 轴将会更新这些值。

    const yAxisMaxValues = d3.max(bins, (d) => {
      return d.length
    }) as number
    yAxis.domain([0, yAxisMaxValues])

接下来,画 y 轴。

    yAxisGroupNode.transition().duration(750).call(d3.axisLeft(yAxis))

对于条形节点,我们用 bin 数据连接矩形,处理条形以及我们正在重画的新条形。

    const barsNode = histogramChart.selectAll<SVGRectElement, number[]>('rect').data(bins)

    const { height } = this.props

    barsNode
      .enter()
      .append('rect')
      .merge(barsNode) // get existing elements
      .transition() // apply changes
      .duration(750)
      .attr('transform',  (d) => {
        // @ts-ignore
        return `translate(${xAxis(d.x0)},${yAxis(d.length)})`
      })
      .attr('width', (d) => {
        return xAxis((d as Types.BarsNode).x1) - xAxis((d as Types.BarsNode).x0) - 1
      })
      .attr('height', (d) => {
        return height - yAxis(d.length)
      })
      .style('fill', this.props.fill)

最后,如果因为变更而出现额外的小节,我们需要删除它们。

    barsNode.exit().remove()
  }

jsx很简单。

然而,这一次我将使用 Material-UI 排版组件添加标题和标签,以包含我们的文本标签和 SVG 来保存<g>元素和一个 Material-UI 滑块。

我还利用父组件设置的props来整齐地对齐图表。

  render() {
    const { width, height, margin } = this.props
    return (
      <div className="histogram">
        <Typography id="discrete-slider" gutterBottom>
          2020 Eth Price days/price Histogram Chart
        </Typography>
        <svg height={height + margin.top + margin.bottom} width={width + margin.left + margin.right}>
          <text x={margin.left - 35} y={margin.top - 10} fontSize={10}>
            Days
          </text>
          <text x={width + margin.left + 20} y={height + margin.top + 16} fontSize={10}>
            Price
          </text>
          <g className="histogramChart" transform={`translate(${margin.left},${margin.top})`} />
        </svg>
        <div className="sliderDiv">
          <Typography id="discrete-slider" gutterBottom>
            Number of ticks:
          </Typography>
          <Slider
            defaultValue={this.state.numberOfTicks}
            getAriaValueText={(value: number) => {
              return `${value} ticks`
            }}
            valueLabelDisplay="auto"
            min={10}
            max={85}
            onChange={this.handleChange}
          />
        </div>
      </div>
    )
  }
}

该接口将保存数据和对齐属性。

interface IHistogramProps {
  data: Types.Data[]
  margin: {
    top: number
    right: number
    bottom: number
    left: number
  }
  width: number
  height: number
  fill: string
}

状态包含要显示的刻度数。

interface IHistogramState {
  numberOfTicks: number

}

直方图. scss

在我的 SCSS 中,我将为div设置一些填充,并为滑块和 SVG 文本颜色设置属性。

.histogram {
  padding-top: 50px;
}
.sliderDiv {
  width: 400px;
  padding-left: 50px;
  padding-top: 20px;
}
svg text {
  fill: white;
}

App.tsx

最后,我在App.tsx中加入了直方图组件。

在您看到的饼图中,数据是从父组件通过props传递的。

正如我提到的,我们希望从图表组件中提取数据,以备数据在多个组件之间共享。

在这里的图表中,我使用的是d3.dsv。但是,我将数据从App.tsx传递到子组件直方图。看一看:

import React, { useEffect } from 'react'
import './App.scss'
import * as d3 from 'd3'
import Histogram from './components/Histogram/Histogram'
import { Types } from './components/Histogram/types'

function App() {

我正在使用函数状态,所以一旦数据被更新,它将自动反映在直方图组件上。我的数据类型是类型number[],因为我将用价格度量设置一个数组。对于初始值,我可以用([{ 'price': 0 }])

  const [data, setData] = React.useState([{ 'price': 0 }] as Types.Data[])
  useEffect(() => {

在每次渲染时会被多次调用,所以我想限制只加载一次数据。

为此,我可以检查数据是否只有我设置的初始值。由于我用一个数组和一个结果设置了初始值,所以结果比那个多(data.length <= 1),可以检索数据。

    if (data.length <= 1) {
      d3.dsv(',', '/data/historicalPrice.csv', (d) => {
        return {
          price: d.open as unknown as number
        }
      }).then((d) => {

我使用 react set状态机制来设置数据。

        setData(d)
      })
    }
  })
  return (
    <div className="App">
      <header className="App-header">

在渲染中,我用props设置了直方图组件。

        <Histogram data={data} margin={{ top: 20, right: 45, bottom: 20, left: 50 }} width={400} height={400} fill="tomato" />
      </header>
    </div>
  )
}

export default App

正如我们之前所做的,使用 format、lint 和 test 功能来确保质量。

$ yarn format & yarn lint & yarn test

图 4-7 显示了最终结果。

img/510438_1_En_4_Fig7_HTML.jpg

图 4-7

显示以太币分组价格的直方图

看一下图表,似乎在 2020 年的大部分时间里,以太坊的价格要么是 225 美元(约 50 天),要么是 400 美元(约 37 天)。

这就是图表的力量。只要看一眼图表,我就能了解这个故事。

注意,我在本章中使用的图表是基于投资工具的,但我不建议投资本书中的任何股票或硬币。

您可以从这里下载直方图组件的完整代码:

https://github.com/Apress/integrating-d3.js-with-react/tree/main/ch04-05/histogram-d3-ts

摘要

本章是上一章的延续,在这一章中,我介绍了如何使用 TypeScript 作为类型检查器,用 React 和 D3 创建一些简单的图表。

我向您展示了如何使用 React 函数和类组件创建下面的简单图表,重点是让 D3 完成大部分工作:

  • 圆形分格统计图表

  • 散点图

此外,我还向您展示了如何通过集成 D3 和 React 以及添加更多 React 库来创建直方图。

我们使用其他 React 库,如 Material-UI,并从父组件中检索数据,这样数据就可以在多个组件之间共享。

在我的 d3 和 React 交互课程中,你可以看到用不同的方法实现本章所有例子的其他方法。互动课程涵盖了本节的更多主题,例如,对 DOM、交互性、设计的更多控制,以及对本章内容的扩展。该课程灵活地补充了本章和本书; https://elielrom.com/BuildSiteCourse

在下一章中,我们将把 React 状态管理集成到 mix 中,这样我们就可以在整个应用中共享我们的数据,甚至可以与不是来自同一个父组件的多个组件共享。