如何用 160 行代码,实现动态炫酷的可视化图表?

建议 未结 0 209
Gend
Gend 2019-8-23
悬赏:20飞吻
某天在逛社区时看到一帖子:

react-dynamic-charts — A React Library for Visualizing Dynamic Data

这是一个国外大佬在其公司峰会的代码竞赛中写的一个库:react-dynamic-charts,用于根据动态数据创建动态图表可视化。

它的设计非常灵活,允许你控制内部的每个元素和事件。使用方法也非常简单,其源码也是非常精炼,值得学习。

但因其提供了不少API,不利于理解源码。所以以下实现有所精简:

准备通用工具函数

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)`;
}

使用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: 第一次动态渲染时间
内部操作方法和对应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 都可以返回一个清除函数。如此可以将添加和移除订阅的逻辑放在一起。

整理用于渲染页面的数据




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: 每组的原始数据

开始渲染页面

大致的逻辑就是:

根据不同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>
定义常规propTypes和defaultProps



DynamicBarChart.propTypes = {

showTitle: PropTypes.
bool,

iterationTimeout: PropTypes.number,

data: PropTypes.
array,

startRunningTimeout: PropTypes.number,

barHeight: PropTypes.number,

barGapSize: PropTypes.number,

baseline: PropTypes.number,

};



DynamicBarChart.defaultProps = {

showTitle:
true,

iterationTimeout:
200,

data: [],

startRunningTimeout:
0,

barHeight:
50,

barGapSize:
20,

baseline: null,

};




export {

DynamicBarChart

};





如何使用




import React, { Component }
from
"react";




import { DynamicBarChart }
from
"./DynamicBarChart";




import helpers
from
"./helpers";


import mocks
from
"./mocks";




import
"react-dynamic-charts/dist/index.css";




export
default

class
App
extends
Component {

render() {


return (



<
DynamicBarChart


barGapSize=
{10}


data=
{helpers.generateData(100,
mocks.defaultChart, {


prefix: "
Iteration"

})}


iterationTimeout=
{100}


showTitle=
{true}


startRunningTimeout=
{2500}

/>

)

}


}
1. 批量生成Mock数据


helpers.js:


function
getRandomNumber(
min, max) {


return
Math.floor(
Math.random() * (max - min +
1) + min);

};





function
generateData(
iterations =
100, defaultValues = [], namePrefix = {}, maxJump =
100) {


const arr = [];


for (
let i =
0; i <= iterations; i++) {


const values = defaultValues.map(
(
v, idx) => {


if (i ===
0 &&
typeof v.value ===
'number') {


return v;

}


return {

...v,


value: i ===
0 ?
this.getRandomNumber(
1,
1000) : arr[i -
1].values[idx].value +
this.getRandomNumber(
0, maxJump)

}

});

arr.push({


name:
`
${namePrefix.prefix ||
''}
${(namePrefix.initialValue ||
0) + i}`,

values

});

}


return arr;

};




export
default {

getRandomNumber,

generateData

}



mocks.js:




import helpers
from
'./helpers';


const defaultChart = [

{


id:
1,


label:
'Google',


value: helpers.getRandomNumber(
0,
50)

},

{


id:
2,


label:
'Facebook',


value: helpers.getRandomNumber(
0,
50)

},

{


id:
3,


label:
'Outbrain',


value: helpers.getRandomNumber(
0,
50)

},

{


id:
4,


label:
'Apple',


value: helpers.getRandomNumber(
0,
50)

},

{


id:
5,


label:
'Amazon',


value: helpers.getRandomNumber(
0,
50)

},

];


export
default {

defaultChart,

}
一个乞丐版的动态排行榜可视化就做好喇。


完整代码



import React, { useState, useEffect }
from
'react';


import PropTypes
from
'prop-types';


import
'./styles.scss';




const getRandomColor =

() => {


const letters =
'0123456789ABCDEF';


let color =
'#';


for (
let i =
0; i <
6; i++) {

color += letters[
Math.floor(
Math.random() *
16)]

}


return color;

};




const translateY =
(
value) => {


return
`translateY(
${value}px)`;

}




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);


let iterationTimeoutHolder =
null;





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;

});


console.table(highestValue);



setCurrentValues(nextValues);

setHighestValue(highestValue);

setActiveItemIdx(activeItemIdx +
1);

}





function
nextStep (
firstRun = false) {

setFirstRun(firstRun);

setNextValues(www.kaifx.cn);

}



useEffect(

() => {

setDataQueue(props.data);

}, []);



useEffect(

() => {

start();

}, [dataQueue]);



useEffect(

() => {

iterationTimeoutHolder =
window.setTimeout(nextStep,
1000);


return

() => {


if (iterationTimeoutHolder) {


window.clearTimeout(iterationTimeoutHolder);

}

};

}, [activeItemIdx]);




const keys =
Object.keys(currentValues);


const { barGapSize, barHeight, showTitle, data } = props;


console.table(
'data', data);


const maxValue = highestValue /
0.85;
const sortedCurrentValues = keys.sort(
(
a, b) => currentValues[b].value - currentValues[a].value);
const currentItem = dataQueue[activeItemIdx -
1] || {};

return (
<div className="www.gendan5.com">

{

<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>

);

};



DynamicBarChart.propTypes = {

showTitle: PropTypes.bool,

iterationTimeout: PropTypes.number,

data: PropTypes.array,

startRunningTimeout: PropTypes.number,

barHeight: PropTypes.number,

barGapSize: PropTypes.number,

baseline: PropTypes.number,

};



DynamicBarChart.defaultProps = {

showTitle: true,

iterationTimeout: 200,

data: [],

startRunningTimeout: 0,

barHeight: 50,

barGapSize: 20,

baseline: null,

};



export {

DynamicBarChart

};



styles.scss



.live-chart {

width: 100%;

padding: 20px;

box-sizing: border-box;

position: relative;

text-align: center;

h1 {

font-weight: 700;

font-size: 60px;

text-transform: uppercase;

text-align: center;

padding: 20px 10px;

margin: 0;

}



.chart {

position: relative;

margin: 20px auto;

}



.chart-bars {

position: relative;

width: 100%;

}



.bar-wrapper {

display: flex;

flex-wrap: wrap;
align-items: center;
position: absolute;
top: 0;
left: 0;
transform: translateY(0);
transition: transform 0.5s linear;
padding-left: 200px;
box-sizing: border-box;
width: 100%;
justify-content: flex-start;
label {
position: absolute;
height: 100%;
width: 200px;
left: 0;
padding: 0 10px;
box-sizing: border-box;
text-align: right;
top: 50%;
transform: translateY(-50%);
font-size: 16px;
font-weight: 700;
display: flex;
justify-content: flex-end;
align-items: center;
}
.value {
font-size: 16px;
font-weight: 700;
margin-left: 10px;
}
.bar {
width: 0%;
transition: width 0.5s linear;
}
}
}
原项目地址:
react-dynamic-charts:https://dsternlicht.github.io/react-dynamic-charts/
结语
一直对实现动态排行榜可视化感兴趣,无奈多数都是基于D3或echarts实现。
而这个库,不仅脱离图形库,还使用了React 16的新特性。也让我彻底理解了React Hook的妙用。
回帖
  • 消灭零回复