如何用 160 行代码,实现动态炫酷的可视化图表?
admin
2023-04-01 01:21:26
0

准备通用工具函数

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

  1. 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。


{

{
showTitle &&

{currentItem.name}


}

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 (
        
{currentValueData.value}
); }) }



}

定义常规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 (
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);
    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 (


{

{
showTitle &&

{currentItem.name}


}

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

}
}
结语
一直对实现动态排行榜可视化感兴趣,无奈多数都是基于D3或echarts实现。
而这个库,不仅脱离图形库,还使用了React 16的新特性。也让我彻底理解了React Hook的妙用。

相关内容

热门资讯

特朗普下令不打了,美国的“余粮... 可以说戛然而止,快得让很多人都无法适应。前两周,每一天美国都要打击伊朗一通。就在7月24日白宫会议前...
穆杰塔巴最新表态:支持黎真主党 △伊朗最高领袖穆杰塔巴·哈梅内伊(资料图)当地时间7月26日晚,伊朗最高领袖穆杰塔巴·哈梅内伊对黎巴...
美军瘫痪一艘驶向伊朗的油轮,特... 美国总统特朗普虽下令暂停打击伊朗,但美军仍在执行对霍尔木兹海峡的封锁任务。针对美军瘫痪一艘驶向伊朗油...
“小牌大耍”的底气到底是哪里来... 澎湃首席评论员 李平这两天,“艺人李权哲乘高铁占座事件”在网上引发热议,该艺人参加的活动主办方曾发布...
越南籍船舶南海遇险沉没,中方已... 据凤凰卫视报道,一艘越南船舶7月24日夜间在越南庆和以东220海里附近遇险沉没,船上共62名越南籍人...
李权哲占座、蒙面、装死:娱乐圈... 你买了一张高铁一等座的票,找到自己的位置,18A。有人坐着。你拿出手机看了眼购票记录——没错,18A...
杭州景区咖啡店闯入1条幼蛇,仅... 夏天气温高、湿度大,蛇类活动进入高峰期,千万别被“小蛇无害”的刻板印象骗了!有些幼蛇看着迷你可爱,实...
AI开源公开信签署企业增至50... IT之家 7 月 26 日消息,英伟达、微软、IBM、Meta 等 25 家美国科技企业本周(7 月...
南阳快手短视频代运营企业的服务... 在短视频平台快速发展的背景下,企业通过快手等渠道进行内容运营已成为常见的市场动作。对于希望借助短视频...
普通人该如何跟上科技发展的步伐 这个被称作科技的词汇, 听起来既有宏大之感, 又颇为遥远, 好似仅仅归属于在实验室里专心致志钻研的科...