在 React 中使用 D3.js

问题

现在很多项目使用的是 React 或 Vue,而 D3.js 却是直接操作 DOM 的,所以想在这些框架中使用 D3.js 不能使用一般的方式。下文以一个饼图为例介绍两种不错的方案。

PositionsPie

方案 1: D3.js 做计算

一种方案是只使用 D3.js 做计算,使用 React 根据计算值生成 DOM。

import * as React from 'react';
import * as d3 from 'd3';
import { PieArcDatum } from 'd3';

import { str2rgb } from 'utils';

interface PositionData {
  symbol: string;
  val: number;
}

interface PositionsPieProps {
  positions: PositionData[];
  width: number;
  height: number;
}

const PositionsPie = (props: PositionsPieProps) => {
  const { positions, width, height } = props;
  const radius = Math.min(width, height) / 2;

  const pie = d3
    .pie<PositionData>().sort(null)
    .value(d => d.val)(positions);

  const arcPathGen = d3.arc<PieArcDatum<PositionData>>()
    .innerRadius(0)
    .outerRadius(radius - 10);

  return (
    <svg width={width} height={height}>
      <g transform={`translate(${width / 2}, ${height / 2})`}>
        {
          pie.map((val, idx) => {
            const arcPath = arcPathGen(val) as string;

            const labelPath = d3.arc<PieArcDatum<PositionData>>()
              .outerRadius(radius - 40)
              .innerRadius(radius - 40);

            return (
              <g key={idx}>
                <path d={arcPath} fill={str2rgb(val.data.symbol)} />
                <text
                  dy="0.35em"
                  style={{ textAnchor: 'middle' }}
                  transform={`translate(${labelPath.centroid(val)})`}
                >
                  {val.data.symbol}
                </text>
              </g>
            );
          })
        }
      </g>
    </svg>
  );
};

export default PositionsPie;

d3.pie()d3.arc() 都可以用来做计算操作,生成绘制 SVG 所用的数据。

方案 2: D3.js 操作类 DOM 结构

react-faux-dom 这个库可以生成类似 DOM 的数据结构来给 D3.js 操作,然后渲染成 React 元素。

const PositionsPie = (props: PositionsPieProps) => {
  const { positions, width, height } = props;
  const radius = Math.min(width, height) / 2;

  const el = ReactFauxDOM.createElement('svg');

  const svg = d3.select(el);
  svg.attr('width', width).attr('height', height);
  const g = svg.append('g');
  g.attr('transform', `translate(${width / 2}, ${height / 2})`);
  const pie = d3.pie<PositionData>().sort(null)
    .value(d => d.val);

  const path = d3.arc<PieArcDatum<PositionData>>()
    .innerRadius(0)
    .outerRadius(radius - 10);

  const label = d3.arc<PieArcDatum<PositionData>>()
    .outerRadius(radius - 40)
    .innerRadius(radius - 40);

  const arc = g.selectAll('.arc')
    .data(pie(positions))
    .enter().append('g')
    .attr('class', 'arc');

  arc.append('path')
    .attr('d', path)
    .attr('fill', d => str2rgb(d.data.symbol));

  arc.append('text')
    .attr('transform', d => `translate(${label.centroid(d)})`)
    .attr('dy', '0.35em')
    .attr('style', 'textAnchor: middle')
    .text(d => d.data.symbol);

  return el.toReact();
};

除了能够像 D3.js 默认的方式那样进行操作外,还支持动画。