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

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

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

640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1


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

640?wx_fmt=gif&tp=webp&wxfrom=5&wx_lazy=1

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

但因其提供了不少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: 经过处理后用于渲染的数据数组

    640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1
  • 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: 每组的原始数据

开始渲染页面


大致的逻辑就是:

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

  2. 计算宽度,返回每项的label、bar、value

  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>

定义常规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数据

640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1

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( 11000) : 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 = [
  {
     id1,
     label'Google',
     value: helpers.getRandomNumber( 050)
  },
  {
     id2,
     label'Facebook',
     value: helpers.getRandomNumber( 050)
  },
  {
     id3,
     label'Outbrain',
     value: helpers.getRandomNumber( 050)
  },
  {
     id4,
     label'Apple',
     value: helpers.getRandomNumber( 050)
  },
  {
     id5,
     label'Amazon',
     value: helpers.getRandomNumber( 050)
  },
];
export  default {
  defaultChart,
}
一个乞丐版的动态排行榜可视化就做好喇。

640?wx_fmt=gif&tp=webp&wxfrom=5&wx_lazy=1

完整代码


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

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

640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1

结语


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


来自 “ ITPUB博客 ” ,链接:http://blog.itpub.net/69946279/viewspace-2654610/,如需转载,请注明出处,否则将追究法律责任。

转载于:http://blog.itpub.net/69946279/viewspace-2654610/

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值