使用 React Hook 高效实现动态炫酷的可视化图表

Posted 程序员依扬

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了使用 React Hook 高效实现动态炫酷的可视化图表相关的知识,希望对你有一定的参考价值。

某天在逛社区时看到一帖子:

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($valuepx)`;

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. 整理用于渲染页面的数据

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. 开始渲染页面…

大致的逻辑就是:

  1. 根据不同Props,循环排列后的数据:sortedCurrentValues

  2. 计算宽度,返回每项的labelbarvalue

  3. 根据计算好的高度,触发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>

6. 定义常规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
;

7. 如何使用

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,

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

8. 完整代码

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($valuepx)`;


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

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


 

原项目地址:react-dynamic-charts:https://dsternlicht.github.io/react-dynamic-charts/

结语

一直对实现动态排行榜可视化感兴趣,无奈多数都是基于D3echarts实现。React 16的新特性。也让我彻底理解了React Hook的妙用。


❤️ 看完三件事

如果你觉得这篇内容对你挺有启发,我想邀请你帮我三个小忙:

  1. 点个「在看」,让更多的人也能看到这篇内容(喜欢不点在看,都是耍流氓 -_-)

  2. 关注我的官网 https://muyiy.cn,让我们成为长期关系

  3. 关注公众号「高级前端进阶」,每周重点攻克一个前端面试重难点,公众号后台回复「面试题」 送你高级前端面试题。

以上是关于使用 React Hook 高效实现动态炫酷的可视化图表的主要内容,如果未能解决你的问题,请参考以下文章

教你轻松实现炫酷的动态数据可视化

并非遥不可及,这5种炫酷的动态图用 Python 轻松实现

这5种炫酷的动态可视化图形,Python 制作起来特简单

推荐 8 个炫酷的数据可视化大屏项目!

推荐 8 个炫酷的数据可视化大屏项目!

还在用饼状图?来瞧瞧这些炫酷的百分比可视化新图形(附代码实现)⛵