使用React-Vis实现对数据的友好可视化

首先

本文是React #2 Advent Calendar 2019第18天的文章。

正如标题所示,本文涉及使用React进行数据可视化的内容。
如果你想用React来开发涉及仪表盘和数据可视化的项目,我推荐React-Vis这个库,它非常不错!

我在使用React进行产品开发时,想找一个根据React生命周期和组件设计制作的可视化库,结果找到了React-Vis。如果有类似的需求,也许这个库对你有参考价值。(最初也是基于类似的理由使用recharts库)

“React-Vis是什么?”

Uber公司的一个开源项目,在GitHub上公开。尽管在2019年12月18日时有6.6k个Star(以前者为准),但我认为找不到相关的日文文章,所以如果有使用它的人,请告诉我。

GitHub链接: https://github.com/uber/react-vis
官方网站链接: https://uber.github.io/react-vis/

为什么选择React-Vis?

如果要可视化数据,有一些著名的库,如D3、ChartJS和ThreeJS等。这些库的表达能力往往超过React-Vis,并且足够使用。因此,在许多情况下并不需要使用专门针对React的库。但从个人角度来看,我还是

1. 可以以 React 组件的形式进行编写和组装的特点。
2. 性能。

我对其感到吸引。

首先,如果在图表的构成要素(如标签、轴范围和动画等)中注入状态(State),通过更新状态来实时更新图表的绘制,就像React组件一样设计和编写,这让人感到非常方便。

其次,从直觉上讲,在单页应用程序(SPA)中,当绘制多个数据点较多的图表(如1,000或10,000个)时,会感受到性能上的差异。但这个问题在后端的数据处理也很重要,并且由于缺乏比较验证测试,所以我将在以后详细介绍这个差异。

此外,作为一个使用React进行产品开发的人来说,我也对React-Vis所提出的以下原则感到吸引。

[使用 React 轻松操作]
React-Vis 的设计与 React 组件类似,可以通过属性、子组件和回调函数进行构建。

【高水平和可定制性】
React-Vis可以通过简单的代码和默认设置创建复杂的图表,同时也可以按照您喜欢的方式自定义每个组件。

[強大的行业]
React-Vis是为了支持Uber的各种内部工具而开发的。

所有这些东西,都是我在图书馆中寻求的,但是对我来说,第三个强大的行业是相当重要的。
Uber的产品以其地理空间信息和机器学习这些大数据的可视化特点,与之紧密相邻的情况下,开发了对它们性能的重视(也许是)库,这是非常有吸引力的。(出于个人原因,由于处理了一些相似的产品)

反过来说,不安的一点是还没有支持Typescript。虽然有些热心的人提供了react-vis.d.ts文件,但是由于没有@types支持,有时候我还得自己加上类型。

你可以用什么样的方式写?

虽然开场白有点长,但我会以最简单的方式画出典型的图表。

线系列(折线图)

import React from "react";
import { XYPlot, LineSeries } from "react-vis";

interface SamplePropsTypes {
  width: number;
  height: number;
}

interface DataTypes {
  x: number;
  y: number;
}

const SampleLine = (props: SamplePropsTypes) => {
  const data: DataTypes[] = [
    { x: 0, y: 18 },
    { x: 1, y: 19 },
    { x: 2, y: 20 },
    { x: 3, y: 21 },
    { x: 4, y: 22 },
    { x: 5, y: 23 },
    { x: 6, y: 24 },
    { x: 7, y: 25 },
  ];

  return (
    <div>
      React Advent2 18th
      <XYPlot width={props.width} height={props.height}>
        <LineSeries data={data} />
      </XYPlot>
    </div>
  );
};
image.png

垂直柱状图(VerticalBarSeries)

// 上記コードに追加・変更
import { VerticalBarSeries } from "react-vis";

return (
  <div>
    React Advent2 18th
   <XYPlot width={props.width} height={props.height}>
     <VerticalBarSeries data={data} />
   </XYPlot>
  </div>
)
image.png

水平柱状图(HorizontalBarSeries 横向版本)

// 上記コードに追加・変更
import { HorizontalBarSeries } from "react-vis";

const dataHorizontal: DataTypes[] = [
  { y: 0, x: 18 },
  { y: 1, x: 19 },
  { y: 2, x: 20 },
  { y: 3, x: 21 },
  { y: 4, x: 22 },
  { y: 5, x: 23 },
  { y: 6, x: 24 },
  { y: 7, x: 25 },
];

return (
  <div>
    React Advent2 18th
    <XYPlot width={props.width} height={props.height}>
      <HorizontalBarSeries data={dataHorizontal} />
    </XYPlot>
  </div>
);
image.png

由于水平条形图的轴会互换,所以输入的数据的x和y也会交换位置,有点复杂。

因为各个部分都作为React组件进行了导出,所以可以直观地使用LineSeries来嵌入参考线等部分,感觉非常不错呢。

插入参考线

  return (
    <div>
      React Advent2 18th
      <XYPlot width={props.width} height={props.height}>
        <VerticalBarSeries data={data} />
        <LineSeries data={data} />
      </XYPlot>
    </div>
  );
image.png

如果稍微增添一些细节,它会变成这样。

import React, { useState, useEffect } from "react";
import { makeStyles } from "@material-ui/core/styles";
import Button from "@material-ui/core/Button";
import {
  HorizontalBarSeries,
  XYPlot,
  XAxis,
  YAxis,
  Crosshair,
} from "react-vis";
import { descSort, ascSort } from "./utils";
import { sample1, sample2, sampleLabel } from "./sampleData";

const useStyles = makeStyles(theme => ({
  crosshair: {
    color: "white",
    backgroundColor: "black",
    width: 20,
    opacity: 0.7,
    paddingLeft: 10,
  },
}));

interface SamplePropsTypes {
  width: number;
  height: number;
}

interface DataTypes {
  x: number;
  y: number;
}

const SampleVis = (props: SamplePropsTypes) => {
  const classes = useStyles();
  const sampleLabel = ["R", "e", "a", "c", "t", "-", "A", "C"];

  const [crosshairValues, setCrosshairValues] = useState<any>({});
  const [data, setData] = useState<DataTypes[]>([]);
  const [isLabel, setLabel] = useState<boolean>(false);
  const [isSort, setSort] = useState<boolean>(false);
  const [labelList, setLabelList] = useState<string[]>([]);
  const [top, setTop] = useState<number>(0);
  const [yLabel, setYLabel] = useState<number | null>(null);

  const getData = (direction: string) => {
    if (direction === "vertical") {
      setData(sample1);
    } else if (direction === "horizontal") {
      setData(sample2);
    }
  };

  useEffect(() => {
    getData("horizontal");
  }, []);

  useEffect(() => {
    if (isLabel) {
      setLabelList(sampleLabel);
    } else {
      setLabelList([]);
    }
  }, [isLabel]);

  useEffect(() => {
    const tmpLabel = labelList.slice().reverse();
    if (isSort) {
      data.sort((a: any, b: any) => ascSort(a.x, b.x));
      data.map((val: any, index: number) => (val.y = index));
      setLabelList(tmpLabel);
    } else {
      data.sort((a: any, b: any) => descSort(a.x, b.x));
      data.map((val: any, index: number) => (val.y = index));
      setLabelList(tmpLabel);
    }
  }, [isSort]);

  const onMouseLeave = () => {
    setCrosshairValues({});
    setYLabel(null);
    setTop(0);
  };

  const onNearestX = (_value: any, { event, innerX, innerY, index }: any) => {
    console.log(`${innerY} | ${innerX} | ${index}`);
    setYLabel(index);
    setTop(event.offsetY);
    setCrosshairValues(data[index]);
  };

  return (
    <div>
      <Button onClick={() => setLabel(!isLabel)}>Change Label</Button>
      <Button onClick={() => setSort(!isSort)}>Change Sort</Button>
      {data.length > 0 ? (
        <XYPlot
          width={props.width}
          height={props.height}
          onMouseLeave={onMouseLeave}
        >
          <XAxis title="React Advent2" />
          <YAxis
            title="18th"
            tickFormat={v => {
              if (v > labelList.length) {
                return null;
              }
              return labelList[v];
            }}
          />
          <HorizontalBarSeries
            data={data}
            color="skyblue"
            onNearestXY={onNearestX}
          />
          {yLabel != null ? (
            <Crosshair values={[crosshairValues]}>
              <div
                className={classes.crosshair}
                style={{ position: "absolute", top: top - 30 }}
              >
                <p>{labelList[yLabel]}</p>
                <p>{data[yLabel].x}</p>
              </div>
            </Crosshair>
          ) : (
            <div />
          )}
        </XYPlot>
      ) : (
        ""
      )}
    </div>
  );
};
image.png
image.png

一下子增加了很多组件,但对于熟悉React组件的人来说,写起来可能会比较容易吧?
除了这些基本图表之外,还提供了各种各样的图表(散点图、面积图、树状图、网络图等),所以不太会遇到绘图困难的情况。

这次省略了,但是我认为,得益于Hooks等,构建诸如筛选器等互动操作逻辑的部分会更易于编写。

最後+的是

我們簡單介紹了使用React-Vis進行數據可視化的方法。
該庫的原則是,每個部件都被導出為React元件,所以可以在JSX中將各種部件嵌入到XyPlot中,從而創建包含任意部件的圖表,這非常好。此外,每個部件都沒有多餘的東西,並且是分離的,具有高度的自由度,這也很好。但是,可能會增加代碼的編寫量?這方面我們希望能夠進行良好的泛化處理。
但是,有時候文檔中未涵蓋的部分也存在一些問題,需要查看源代碼才能明確理解如何自定義,但由於它是以React元件的形式設計的,所以它相對比較容易理解。
在處理數據可視化時,請一定要嘗試使用React。

bannerAds