準(zhǔn)備通用工具函數(shù)
成都創(chuàng)新互聯(lián)公司一直在為企業(yè)提供服務(wù),多年的磨煉,使我們?cè)趧?chuàng)意設(shè)計(jì),全網(wǎng)整合營(yíng)銷(xiāo)推廣到技術(shù)研發(fā)擁有了開(kāi)發(fā)經(jīng)驗(yàn)。我們擅長(zhǎng)傾聽(tīng)企業(yè)需求,挖掘用戶對(duì)產(chǎn)品需求服務(wù)價(jià)值,為企業(yè)制作有用的創(chuàng)意設(shè)計(jì)體驗(yàn)。核心團(tuán)隊(duì)擁有超過(guò)十載以上行業(yè)經(jīng)驗(yàn),涵蓋創(chuàng)意,策化,開(kāi)發(fā)等專(zhuān)業(yè)領(lǐng)域,公司涉及領(lǐng)域有基礎(chǔ)互聯(lián)網(wǎng)服務(wù)聯(lián)通機(jī)房服務(wù)器托管、重慶APP開(kāi)發(fā)、手機(jī)移動(dòng)建站、網(wǎng)頁(yè)設(shè)計(jì)、網(wǎng)絡(luò)整合營(yíng)銷(xiāo)。
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)
;
}
使用useState Hook聲明狀態(tài)變量
我們開(kāi)始編寫(xiě)組件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);
// 其它代碼...
}
// 動(dòng)態(tài)跑起來(lái)~
function start () {
if (activeItemIdx > 1) {
return;
}
nextStep(true);
}
// 對(duì)下一幀數(shù)據(jù)進(jìn)行處理
function setNextValues () {
// 沒(méi)有幀數(shù)時(shí)(即已結(jié)束),停止渲染
if (!dataQueue[activeItemIdx]) {
iterationTimeoutHolder = null;
return;
}
// 每一幀的數(shù)據(jù)數(shù)組
const roundData = dataQueue[activeItemIdx].values;
const nextValues = {};
let highestValue = 0;
// 處理數(shù)據(jù),用作最后渲染(各種樣式,顏色)
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;
});
// 屬性的操作,觸發(fā)useEffect
setCurrentValues(nextValues);
setHighestValue(highestValue);
setActiveItemIdx(activeItemIdx + 1);
}
// 觸發(fā)下一步,循環(huán)
function nextStep (firstRun = false) {
setFirstRun(firstRun);
setNextValues();
}
對(duì)應(yīng)useEffect:
// 取原始數(shù)據(jù)
useEffect(() => {
setDataQueue(props.data);
}, []);
// 觸發(fā)動(dòng)態(tài)
useEffect(() => {
start();
}, [dataQueue]);
// 設(shè)觸發(fā)動(dòng)態(tài)間隔
useEffect(() => {
iterationTimeoutHolder = window.setTimeout(nextStep, 1000);
return () => {
if (iterationTimeoutHolder) {
window.clearTimeout(iterationTimeoutHolder);
}
};
}, [activeItemIdx]);
useEffect示例:
useEffect(() => {
document.title = You clicked ${count} times
;
}, [count]); // 僅在 count 更改時(shí)更新
為什么要在 effect 中返回一個(gè)函數(shù)?
這是 effect 可選的清除機(jī)制。每個(gè) effect 都可以返回一個(gè)清除函數(shù)。如此可以將添加和移除訂閱的邏輯放在一起。
整理用于渲染頁(yè)面的數(shù)據(jù)
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: 每組數(shù)據(jù)的索引
maxValue: 圖表最大寬度
sortedCurrentValues: 對(duì)每組數(shù)據(jù)進(jìn)行排序,該項(xiàng)影響動(dòng)態(tài)渲染。
currentItem: 每組的原始數(shù)據(jù)
開(kāi)始渲染頁(yè)面
大致的邏輯就是:
根據(jù)不同Props,循環(huán)排列后的數(shù)據(jù):sortedCurrentValues
計(jì)算寬度,返回每項(xiàng)的label、bar、value
根據(jù)計(jì)算好的高度,觸發(fā)transform。
${width}%
; return (
{currentValueData.value}
);
})
}
定義常規(guī)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 (
data={helpers.generateData(100, mocks.defaultChart, {
prefix: "Iteration"
})}
iterationTimeout={100}
showTitle={true}
startRunningTimeout={2500}
/>
)
}
}
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,
}
一個(gè)乞丐版的動(dòng)態(tài)排行榜可視化就做好喇。
完整代碼
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);
www.kaifx.cn
setCurrentValues(nextValues);
setHighestValue(highestValue);
setActiveItemIdx(activeItemIdx + 1);
}
function nextStep (firstRun = false) {
setFirstRun(firstRun);
setNextValues();
}
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 (
${width}%
; return (
{currentValueData.value}
);
})
}
);
};
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;
h2 {
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;
}
}
}
結(jié)語(yǔ)
一直對(duì)實(shí)現(xiàn)動(dòng)態(tài)排行榜可視化感興趣,無(wú)奈多數(shù)都是基于D3或echarts實(shí)現(xiàn)。
而這個(gè)庫(kù),不僅脫離圖形庫(kù),還使用了React 16的新特性。也讓我徹底理解了React Hook的妙用。