手把手教你JS實現動態炫酷的可視化圖表


手把手教你JS實現動態炫酷的可視化圖表


前言

某天在逛社區時看到一帖子:

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



手把手教你JS實現動態炫酷的可視化圖表

這是一個國外大佬在其公司峰會的代碼競賽中寫的一個庫:react-dynamic-charts,用於根據動態數據創建動態圖表可視化。

手把手教你JS實現動態炫酷的可視化圖表

它的設計非常靈活,允許你控制內部的每個元素和事件。使用方法也非常簡單,其源碼也是非常精煉,值得學習。


但因其提供了不少API,不利於理解源碼。所以以下實現有所精簡:

1. 準備通用工具函數


手把手教你JS實現動態炫酷的可視化圖表


1. getRandomColor:隨機顏色

<code>const getRandomColor = () => {
const letters = '0123456789ABCDEF';
let color = '#';
for (let i = 0; i < 6; i++) {
color += letters[Math.floor(Math.random() * 16)]
}
return color;

};
複製代碼/<code>

2. translateY:填充Y軸偏移量

<code>const translateY = (value) => {
return `translateY(${value}px)`;
}
複製代碼/<code>

2. 使用useState Hook聲明狀態變量

我們開始編寫組件DynamicBarChart

<code>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);
// 其它代碼...
}
複製代碼/<code>

1. useState的簡單理解:

<code>const [屬性, 操作屬性的方法] = useState(默認值);
複製代碼/<code>

2. 變量解析

  • dataQueue:當前操作的原始數據數組
  • activeItemIdx: 第幾“幀”
  • highestValue: “榜首”的數據值
  • currentValues: 經過處理後用於渲染的數據數組
  • firstRun: 第一次動態渲染時間

3. 內部操作方法和對應useEffect

請配合註釋食用

<code>// 動態跑起來~
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();
}
複製代碼/<code>

對應useEffect:

<code>// 取原始數據
useEffect(() => {
setDataQueue(props.data);
}, []);
// 觸發動態
useEffect(() => {
start();
}, [dataQueue]);
// 設觸發動態間隔
useEffect(() => {
iterationTimeoutHolder = window.setTimeout(nextStep, 1000);
return () => {
if (iterationTimeoutHolder) {
window.clearTimeout(iterationTimeoutHolder);
}
};
}, [activeItemIdx]);
複製代碼/<code>

useEffect示例:

<code>useEffect(() => {
document.title = `You clicked ${count} times`;
}, [count]); // 僅在 count 更改時更新
複製代碼/<code>

為什麼要在 effect 中返回一個函數?

這是 effect 可選的清除機制。每個 effect 都可以返回一個清除函數。如此可以將添加和移除訂閱的邏輯放在一起。

4. 整理用於渲染Dom的數據

<code>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] || {};
複製代碼/<code>
  • keys: 每組數據的索引
  • maxValue: 圖表最大寬度
  • sortedCurrentValues: 對每組數據進行排序,該項影響動態渲染。
  • currentItem: 每組的原始數據

5. 開始渲染...

大致的邏輯就是:

  1. 根據不同Props,循環排列後的數據:sortedCurrentValues
  2. 計算寬度,返回每項的label、bar、value
  3. 根據計算好的高度,觸發transform。
<code>

{
<react.fragment>
{
showTitle &&

{currentItem.name}


}


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

<label>
{
!currentValueData.label
? key
: currentValueData.label
}
/<label>

{currentValueData.value}

);
})
}

/
/<react.fragment>
}

複製代碼/<code>

6. 定義常規propTypes和defaultProps:

<code>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
};

複製代碼/<code>

7. 如何使用

<code>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}
/>
)
}
}
複製代碼/<dynamicbarchart>/<code>

1. 批量生成Mock數據


手把手教你JS實現動態炫酷的可視化圖表

helpers.js:


<code>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
}
複製代碼/<code>

mocks.js:

<code>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,
}
複製代碼/<code>

一個乞丐版的動態排行榜可視化就做好喇。

手把手教你JS實現動態炫酷的可視化圖表


8. 完整代碼

<code>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();
}

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 (

{
<react.fragment>
{
showTitle &&

{currentItem.name}


}


{

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 (

<label>
{
!currentValueData.label
? key
: currentValueData.label
}
/<label>

{currentValueData.value}

);
})
}

/
/<react.fragment>
}

);
};

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

複製代碼/<code>

styles.scss:

<code>.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;
}
}
}
複製代碼/<code>

原項目地址:react-dynamic-charts

https://github.com/dsternlicht/react-dynamic-charts

手把手教你JS實現動態炫酷的可視化圖表


結語

一直對實現動態排行榜可視化感興趣,無奈多數都是基於D3或echarts實現。 而這個庫,不僅脫離圖形庫,還使用了React 16的新特性。也讓我徹底理解了React Hook的妙用。


手把手教你JS實現動態炫酷的可視化圖表


❤️ 看完三件事

如果你覺得這篇內容對你挺有啟發,我想邀請你幫我三個小忙:

  1. 點贊,讓更多的人也能看到這篇內容(收藏不點贊,都是耍流氓 -_-)
  2. 關注公眾號「前端勸退師」,不定期分享原創知識。
  3. 原鏈接:https://juejin.im/post/5d565015f265da03eb13c575


分享到:


相關文章: