图表
前言
某天在逛社区时看到一帖子:
react-dynamic-charts — A React Library for Visualizing Dynamic Data
这是一个国外大佬在其公司峰会的代码竞赛中写的一个库:react-dynamic-charts
,用于根据动态数据创建动态图表可视化。
它的设计非常灵活,允许你控制内部的每个元素和事件。使用方法也非常简单,其源码也是非常精炼,值得学习。
但因其提供了不少API
,不利于理解源码。所以以下实现有所精简:
1. 准备通用工具函数
1. getRandomColor
:随机颜色
const getRandomColor = () => {
const letters = '0123456789ABCDEF';
let color = '#';
for (let i = 0; i < 6; i++) {
color += letters[Math.floor(Math.random() * 16)]
}
return color;
};
2. translateY
:填充 Y 轴偏移量
const translateY = (value) => {
return `translateY(${value}px)`;
}
2. 使用useState Hook
声明状态变量
我们开始编写组件DynamicBarChart
const DynamicBarChart = (props) => {
const [dataQueue, setDataQueue] = useState([]);
const [activeItemIdx, setActiveItemIdx] = useState(0);
const [highestValue, setHighestValue] = useState(0);
const [currentValues, setCurrentValues] = useState({});
const [firstRun, setFirstRun] = useState(false);
// 其它代码...
}
1. useState
的简单理解:
const [属性, 操作属性的方法] = useState(默认值);
2. 变量解析
dataQueue
:当前操作的原始数据数组activeItemIdx
: 第几“帧”highestValue
: “榜首”的数据值currentValues
: 经过处理后用于渲染的数据数组firstRun
: 第一次动态渲染时间
3. 内部操作方法和对应useEffect
请配合注释食用
// 动态跑起来~
function start () {
if (activeItemIdx > 1) {
return;
}
nextStep(true);
}
// 对下一帧数据进行处理
function setNextValues () {
// 没有帧数时(即已结束),停止渲染
if (!dataQueue[activeItemIdx]) {
iterationTimeoutHolder = null;
return;
}
// 每一帧的数据数组
const roundData = dataQueue[activeItemIdx].values;
const nextValues = {};
let highestValue = 0;
// 处理数据,用作最后渲染(各种样式,颜色)
roundData.map((c) => {
nextValues[c.id] = {
...c,
color: c.color || (currentValues[c.id] || {}).color || getRandomColor()
};
if (Math.abs(c.value) > highestValue) {
highestValue = Math.abs(c.value);
}
return c;
});
// 属性的操作,触发useEffect
setCurrentValues(nextValues);
setHighestValue(highestValue);
setActiveItemIdx(activeItemIdx + 1);
}
// 触发下一步,循环
function nextStep (firstRun = false) {
setFirstRun(firstRun);
setNextValues();
}
对应useEffect
:
// 取原始数据
useEffect(() => {
setDataQueue(props.data);
}, []);
// 触发动态
useEffect(() => {
start();
}, [dataQueue]);
// 设触发动态间隔
useEffect(() => {
iterationTimeoutHolder = window.setTimeout(nextStep, 1000);
return () => {
if (iterationTimeoutHolder) {
window.clearTimeout(iterationTimeoutHolder);
}
};
}, [activeItemIdx]);
useEffect
示例:
useEffect(() => {
document.title = `You clicked ${count} times`;
}, [count]); // 仅在 count 更改时更新
为什么要在 effect
中返回一个函数?
这是 effect
可选的清除机制。每个 effect
都可以返回一个清除函数。如此可以将添加和移除订阅的逻辑放在一起。
4. 整理用于渲染Dom
的数据
const keys = Object.keys(currentValues);
const { barGapSize, barHeight, showTitle } = props;
const maxValue = highestValue / 0.85;
const sortedCurrentValues = keys.sort((a, b) => currentValues[b].value - currentValues[a].value);
const currentItem = dataQueue[activeItemIdx - 1] || {};
keys
: 每组数据的索引maxValue
: 图表最大宽度sortedCurrentValues
: 对每组数据进行排序,该项影响动态渲染。currentItem
: 每组的原始数据
5. 开始渲染...
大致的逻辑就是:
- 根据不同
Props
,循环排列后的数据:sortedCurrentValues
- 计算宽度,返回每项的
label
、bar
、value
- 根据计算好的高度,触发
transform
。
<div className="live-chart">
{
<React.Fragment>
{
showTitle &&
<h1>{currentItem.name}</h1>
}
<section className="chart">
<div className="chart-bars" style={{ height: (barHeight + barGapSize) * keys.length }}>
{
sortedCurrentValues.map((key, idx) => {
const currentValueData = currentValues[key];
const value = currentValueData.value
let width = Math.abs((value / maxValue * 100));
let widthStr;
if (isNaN(width) || !width) {
widthStr = '1px';
} else {
widthStr = `${width}%`;
}
return (
<div className={`bar-wrapper`} style={{ transform: translateY((barHeight + barGapSize) * idx), transitionDuration: 200 / 1000 }} key={`bar_${key}`}>
<label>
{
!currentValueData.label
? key
: currentValueData.label
}
</label>
<div className="bar" style={{ height: barHeight, width: widthStr, background: typeof currentValueData.color === 'string' ? currentValueData.color : `linear-gradient(to right, ${currentValueData.color.join(',')})` }} />
<span className="value" style={{ color: typeof currentValueData.color === 'string' ? currentValueData.color : currentValueData.color[0] }}>{currentValueData.value}</span>
</div>
);
})
}
</div>
</section>
</React.Fragment>
}
</div>