使用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>
);
};

垂直柱状图(VerticalBarSeries)
// 上記コードに追加・変更
import { VerticalBarSeries } from "react-vis";
return (
<div>
React Advent2 18th
<XYPlot width={props.width} height={props.height}>
<VerticalBarSeries data={data} />
</XYPlot>
</div>
)

水平柱状图(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>
);

由于水平条形图的轴会互换,所以输入的数据的x和y也会交换位置,有点复杂。
因为各个部分都作为React组件进行了导出,所以可以直观地使用LineSeries来嵌入参考线等部分,感觉非常不错呢。
插入参考线
return (
<div>
React Advent2 18th
<XYPlot width={props.width} height={props.height}>
<VerticalBarSeries data={data} />
<LineSeries data={data} />
</XYPlot>
</div>
);

如果稍微增添一些细节,它会变成这样。
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>
);
};


一下子增加了很多组件,但对于熟悉React组件的人来说,写起来可能会比较容易吧?
除了这些基本图表之外,还提供了各种各样的图表(散点图、面积图、树状图、网络图等),所以不太会遇到绘图困难的情况。
这次省略了,但是我认为,得益于Hooks等,构建诸如筛选器等互动操作逻辑的部分会更易于编写。
最後+的是
我們簡單介紹了使用React-Vis進行數據可視化的方法。
該庫的原則是,每個部件都被導出為React元件,所以可以在JSX中將各種部件嵌入到XyPlot中,從而創建包含任意部件的圖表,這非常好。此外,每個部件都沒有多餘的東西,並且是分離的,具有高度的自由度,這也很好。但是,可能會增加代碼的編寫量?這方面我們希望能夠進行良好的泛化處理。
但是,有時候文檔中未涵蓋的部分也存在一些問題,需要查看源代碼才能明確理解如何自定義,但由於它是以React元件的形式設計的,所以它相對比較容易理解。
在處理數據可視化時,請一定要嘗試使用React。