d3.js 与 react.js

本文简单总结一下 d3(v4) 在 react 中如何使用,如果你还不知道 d3.js 是什么,请移步 d3.js
d3.js 官方样例中的用法都是类似

d3.select("body")
  .selectAll("p")
  .data([4, 8, 15, 16, 23, 42])
  .enter().append("p")
    .text(function(d) { return "I’m number " + d + "!"; });

确实很简明,但是在 react 中却没法下手,所以在 react 中,只需将 d3.js 视为提供图形算法的库即可,不需要掌握如何使用 d3.js 操作 dom。

使用 svg 画一个 bar 图

我们先使用 svg 来画一个最简单的没有坐标轴的 bar 图,理解一下 d3 与 react 的分工。

首先假设我们的 bar 图大小为 1280(px) * 800(px),数据是

const data = [
  { city: '北京', amount: 1000, },
  { city: '上海', amount: 803, },
  { city: '广州', amount: 440, },
  { city: '深圳', amount: 780, },
];

然后我们需要理解 bar 图是如何构成(画)的,bar 图有两个坐标轴:

  • 横轴为分组,对应 city ,决定每根柱子的 x 位置
  • 竖轴为数值,对应 amount,决定每根柱子的高度即 y 的位置

d3 处理图形的算法和坐标计算很方便,使用 d3.scaleBand 将 city 与图的宽度(1280px) 做出映射关系:

const xAxis = d3.scaleBand()
  .range([0, 1280])
  .domain(['北京', '上海', '广州', '深圳'])

就可以用 xAxis('上海') 得到上海这根柱子的 x 位置。每根柱子的宽度使用 xAxis.bandwidth() 获得。

使用 d3.scaleLinear 将 amount 与图的高度(800px) 做出映射关系:

const yAxis = d3.scaleLinear()
  .range([800, 0])
  .domain([0, 1000])

就可以用 yAxis(803) 得到上海这根柱子的高度 y 位置。请注意 range([800, 0]),之所以是反向的,是因为 svg 的纵坐标系是反向的,而且 canvas 的纵坐标系也是反向的。

接下来创建我们的组件

import * as d3 from 'd3';
import React, { PureComponent } from 'react';

const width = 1280;
const height = 800;

const data = [
  { city: '北京', amount: 1000, },
  { city: '上海', amount: 803, },
  { city: '广州', amount: 440, },
  { city: '深圳', amount: 780, },
];

const xAxis = d3.scaleBand()
  .range([0, 1280])
  .domain(['北京', '上海', '广州', '深圳']);
const yAxis = d3.scaleLinear()
  .range([800, 0])
  .domain([0, 1000]);

const barWidth = xAxis.bandwidth();

class Bar extends PureComponent {

  render() {
    return <svg width={width} height={height}>
      {data.map(({ city, amount }, index) => {
        const x = xAxis(city);
        const y = yAxis(amount);
        const barHeight = height - y;

        return <rect
          key={index}
          x={x}
          y={y}
          width={barWidth}
          height={barHeight}
          fill={'#bada55'} />;
      })}
    </svg>;
  }

}

这样就完成了一个最简单的(正数) bar 图,需要注意的是其中计算 barHeight 的时候并没有判断 amount 为负数的情况,且没有处理各种边界错误。

性能

上边的例子中我们是直接 map 数据,创建了 n 个 rect 元素,当数据量上千后,性能会下降,不如改为 canvas 画法,使用 canvas 画的逻辑也是一样的,d3.js 只负责图形算法和坐标计算。

不同的是 canvas 无法像 svg 一样直接在 render 函数中画,需要在 componentDidMount 之后执行画图的逻辑。

class Bar extends PureComponent {

  render() {
    return <canvas
      width={width}
      height={height}
      ref={r => this.canvas = r} />;
  }

  componentDidMount() {
    const ctx = this.canvas.getContext('2d');
    ctx.fillStyle = '#bada55';

    data.map(({ city, amount }, index) => {
      const x = xAxis(city);
      const y = yAxis(amount);
      const barHeight = height - y;

      ctx.beginPath();
      ctx.fillRect(x, y, barWidth, barHeight);
      ctx.closePath();
    });
  }

}

总结

通过上边两个例子,相信你已经可以使用 svgcanvas 通过 d3.jsreact 中画出常用的基本可视化图形了。

在项目中,无论是你是要用 d3 自己画图,还是用 echarts/g2/highcharts,重要的是理解图形是如何组成的,如何画出来的,方法有很多。