组件化可视化图表 - Recharts

Recharts 是 2016 年初团队可视化组推出的一款可视化组件库,为基础表格的绘制提供了另外一种可能。

Recharts 含义是重新定义(Redefined)图表。这个名字的背后在于这个图表在设计上带给开发者的是不一样的体验,不仅是用 React 设计,也在于重新定义了组合与配置方式。

Recharts 到今天的版本是 0.9.3,支持 React 0.14.x 或 15.0.x 版本,现在有至少四个国外团队在产品中使用。为方便国际化,文档只有英文官网 Recharts,中文官网还在编写中。如果试用有问题,欢迎给项目提 issue(P.S. 请使用英文来提,谢谢)。

接下来我们会从思想层面来剖析 Recharts 的原理和精髓。

大家可以回顾一下在做图表类的需求时,碰到最纠结的问题是什么?这里列了一些我碰到最多的问题:

  • 配置非常复杂,可配置的内容太多,找不到到底使用什么配置项来达到想要的目的

  • 很多样式无法完全统一,变化很多。这个线图怎么多了条线?这个柱图的“柱子”怎么是个三角形?

那 Recharts 是怎么解决这些问题呢?

  • 声明式的标签,让写图表和写 HTML 一样简单

  • 贴近原生 SVG 的配置项,让配置项更加自然

  • 接口式的 API,解决各种个性化的需求

下面我们将仔细分析这些是怎么实现的。

声明式的标签

在看代码实现之前,我们先看看怎样一步步的根据各自的需求创建一个线图:

首先,通过调用 LineChart 添加一条 dataKeypvLine

const data = [{ name: 'a', uv: 4000, pv: 2400 }, { name: 'b', uv: 3000, pv: 1398 }, ....];

<LineChart width={600} height={300} data={data}>
  <Line dataKey="pv" stroke="black" />
</LineChart>

运行代码后结果如下:

图片描述

然后,我们可以根据自己的需求去丰富这个线图,比如这个线图需要一个 X 轴和 Y 轴,那只需要在 LineChart 下添加一个 XAxisYAxis 标签即可:

const data = [{ name: 'a', uv: 4000, pv: 2400 }, { name: 'b', uv: 3000, pv: 1398 }, ....];

<LineChart width={600} height={300} data={data}>
  <XAxis />
  <YAxis />
  <Line dataKey="pv" stroke="black" />
</LineChart>

运行代码结果如下:

图片描述

大家看到用 Recharts 绘制图表很多时候就想拼积木一样,那 LineChart 内部是如何去识别这些『零件』的呢?我们先来看一个简单的函数:

const getDisplayName = (Comp) => {
  if (!Comp) { return ''; }
  if (typeof Comp === 'string') { return Comp; }
  return Comp.displayName || Comp.name || 'Component';
};

这个方法很简单,可以用来读取某个 ReactComponent 的名称。在 LineChart 的代码实现中,就是根据 ReactComponent 的 displayName 来识别所有的 Children。我们先来看一个工具方法:

const findAllByType = (children, type) => {
  const result = [];
  let types = [];

  if (_.isArray(type)) {
    types = type.map(t => getDisplayName(t));
  } else {
    types = [getDisplayName(type)];
  }

  React.Children.forEach(children, child => {
    const childType = child && child.type && (child.type.displayName || child.type.name);
    if (types.indexOf(childType) !== -1) {
      result.push(child);
    }
  });

  return result;
};

这里 type 可以是 ReactComponent 或者 ReactComponent 数组。而 LineChart 内部实现的时候就是调用这个方法来识别各个『零件』:

    ...
    render() {
        const { children } = this.props;
        const lineItems = findAllByType(children, Line);
        ...
    }

贴近原生的配置项

图表的配置项可以非常多,但是有很多配置项如填充颜色、描边颜色、描边宽度等等这些都是SVG标签原生就支持的属性,为了减小大家的配置的成本,Recharts 的组件会去解析原生的属性。举个例子,一个线图里面有两条曲线,我想给一条曲线设置成虚线,一条设置成实线,我们只需要像原生的 SVG 元素一样设置 stroke-dasharray 属性就行:

const data = [{ name: 'a', uv: 4000, pv: 2400 }, { name: 'b', uv: 3000, pv: 1398 }, ....];

<LineChart width={600} height={300} data={data}>
  <XAxis />
  <YAxis />
  <Line dataKey="pv" stroke="black" strokeDasharray="5 5" />
  <Line dataKey="uv" stroke="black" />
</LineChart>

结果如下:

图片描述

实现原理也比较简单,首先 Recharts 内部维护一份 SVG 元素支持的所有属性,然后在渲染 SVG 元素之前,我们会去解析相应的ReactElement的 props,看看哪些是 SVG 元素能够支持的属性,最终这些属性可以传入到渲染的 SVG 元素中。

const PRESENTATION_ATTRIBUTES = {
    fill: PropTypes.string,
    strokeDasharray: PropTypes.string,
    ...
};
const getPresentationAttributes = (el) => {
  if (!el || _.isFunction(el)) { return null; }

  const props = React.isValidElement(el) ? el.props : el;
  let result = null;

  for (const key in props) {
    if (props.hasOwnProperty(key) && PRESENTATION_ATTRIBUTES[key]) {
      if (!result) {result = {};}
      result[key] = props[key];
    }
  }

  return result;
};

关于更多SVG属性,大家可以参考W3C标准文档

接口式的 API

很多时基础图表往往不能满足所有的要求,那怎么去满足各种个性化的需求成了图表组件必须要考虑的事情。

Recharts 对可能会变化的元素都提供了自定义的接口,以x轴的刻度为例,普通的刻度就是一些文字,在信息图表中,为了让图表更佳的生动,视觉往往希望能够将文字替换成形象的 icon。

对于这种自定义的需求,Recharts 提供了两种方式,第一种是通过 React Element 的方式:

const CustomizedTick = (props) => {
  const { x, y, payload, bgColor, index } = props;

  return (
      <g>
          <circle cx={x} cy={y + 15} r={10} fill={bgColor}/>
      <text x={x} y={y + 22} textAnchor="middle" fill="#fff">{index}</text>
    </g>
  );
};

<LineChart data={data}>
  <XAxis tick={<CustomizedTick />}/>
  <YAxis/>
  <Line dataKey="pv" stroke="black" strokeDasharray="5 5"/>
  <Line dataKey="uv" stroke="black"/>
</LineChart>

图片描述

通过将 tick 设置成一个 React Element,在拿到内部 props 的同时,也可以非常方便的从外部传入 props

第二种自定义的方式是通过 function:

const renderCustomizedTick = (props) => {
  const { x, y, payload, index } = props;

  return (
      <g>
          <circle cx={x} cy={y + 15} r={10} fill="#666"/>
      <text x={x} y={y + 22} textAnchor="middle" fill="#fff">{index}</text>
    </g>
  );
};

<LineChart data={data}>
  <XAxis tick={renderCustomizedTick} />
  <YAxis/>
  <Line dataKey="pv" stroke="black" strokeDasharray="5 5"/>
  <Line dataKey="uv" stroke="black"/>
</LineChart>

这种方法,renderCustomizedTick 中拿到的参数和 CustomizedTickprops 是一样的,当然这种自定义的方法外部传参数会稍微麻烦一些。

看到这里大家可能会好奇内部是怎么去实现?原理也非常简单,我们在内部计算好 tick 的位置等信息,然后判读 tick 参数的类型,实现代码简化如下:

let tickItem;

if (React.isValidElement(tick)) {
  tickItem = React.cloneElement(tick, props);
} else if (_.isFunction(tick)) {
  tickItem = tick(props);
} else {
  tickItem = <text {...props} className="recharts-cartesian-axis-tick-value">{value}</text>;
}

看到这里大家可以发现 Recharts 内部主要做了计算各种 layout 的事,每个区块具体展示什么内容都是可以自定义的。

延伸

到这里我们已经介绍了 Recharts 实现可视化组件的一些核心思想,其实这些思想不只是在可视化组件中可以应用,很多组件也可以考虑利用这种思想来实现,例如表格组件就可以抽取 TableColumn 两个组件,然后大家使用表格也非常简单:

<Table data={data}>
  <Column name="名称" dataKey="name"/>
  <Column name="数量" dataKey="count" align="right" th={<SortableTh order="asc" onChange={handleSort}/>}/>
  <Column name="金额" dataKey="amt" td="float" align="right"/>
</Table>

关于 1.0 版本的发布

我们大约会在本月末,或下月初发布,

  1. 更好的动画支持

  2. 同步文档更新

  3. 增加一些图表的支持

  4. 90% 的测试覆盖率

关于无线支持可能会放到 1.0 之后再考虑,因为 SVG 对手机的兼容性支持度一般。1.0 版本之后,会切分出 React 15.x 的 Recharts。因为 15.x 对 SVG 的支持更加完善。

尽管 web 端已经有不少优秀的可视化库,亦或是图表库,比如 Echarts,highcharts,科学界有 ggplot,他们都是可视化界的前辈。在可视化的探索上,给我们很多启发。我们造 Recharts 的初忠是给 React 社区贡献一个代码更优雅,灵活可装卸的图表库的图表库。

感谢团队可视化组的小伙伴。最后是安利时间,第一款使用 Recharts 的线上项目 阿里指数

阅读 3.6k

推荐阅读
pure render
用户专栏

一群志同道合的小伙伴,分享关于 React, Flux 在实践中的经验与想法为 segmentfault 的小伙伴可以看到文...

270 人关注
19 篇文章
专栏主页