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

b5c4a8926b9eb3ed215e20a2cd280e8a.gif

4d43c514ba37c7f8303ad80718e78310.png

作者 | 前端劝退师

责编 | 伍杏玲

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

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

09bfb0033f9cea3207199393e38b293a.png

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

bce31cd9e774b6becef03154ed42b6b7.gif

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

但因其提供了不少API,不利于理解源码。所以以下实现有所精简:

4b7e9df1912004e26d61a54b8323f2d3.png

准备通用工具函数

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

23e08b19646703375d1748a6e1bac9f9.png

使用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: 经过处理后用于渲染的数据数组

    8a2713ae1041d8d8def1513dfa23183e.png
  • firstRun: 第一次动态渲染时间

2f843e5df036b39850bbb29bbf7339d7.png

内部操作方法和对应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 都可以返回一个清除函数。如此可以将添加和移除订阅的逻辑放在一起。

2a265379208c70c1b3e62ff830463887.png

整理用于渲染页面的数据

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: 每组的原始数据

eb9cb9d4ef61c19a6bc0276b7d3a8e1e.png

开始渲染页面

大致的逻辑就是:

  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>

b017048dcb6854bf523caac616227622.png

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

e9d3c4c1cb5e27711a326d96917625c6.png

如何使用

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 (<DynamicBarChartbarGapSize={10}data={helpers.generateData(100, mocks.defaultChart, {prefix: "Iteration"
            })}iterationTimeout={100}showTitle={true}startRunningTimeout={2500}
          />
      )
  }}

1. 批量生成Mock数据

d8096bb37d847fb8dc5e63efe1b62ab1.png
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,
}
一个乞丐版的动态排行榜可视化就做好喇。
35f2d42c97a32f73aeb9dff1f5fbabc5.gif

82c9c5ce6500ced89850b7d0454d3cde.png

完整代码

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 (

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

                        {
                          !currentValueData.label
                            ? key
                            : currentValueData.label
                        }{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;
  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/

39a2cd6c587f03ea5049d77cba92fd8a.png

6460e7cc80370b0b66aa9dfd5c215370.png

结语

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

声明:本文系作者投稿,版权归作者所有。

4a3dff9e5d0818c957fc3f846c5addd5.png

5G进入元年,物联网发展愈加火爆!

你是否身怀绝技、却无人知晓;别让你的IoT项目再默默无闻了!

继第一届AI优秀案例评选活动之后,2019年案例评选活动再度升级,CSDN将评选出TOP 30优秀IoT案例,赶快扫码参与评选吧!重磅福利,等你来领!

fcf79d0c3ae00c2571491757141eaee6.png

 热 文 推 荐 

☞柬埔寨月薪 4 万敲代码?真相是我差点丢了性命!

☞ 漫画:程序员养生指北

☞ 为什么程序员和产品经理水火不容? | 每日趣闻

☞ 一套代码两端运行不靠谱?是时候放弃 C++ 跨 Android、iOS 端开发! ☞ImageNet错误率小于4%,数据量依然不够,N-Shot Learning或是终极解决之道? ☞ Python冷知识,不一样的技巧带给你不一样的乐趣 ☞ kafka系统设计开篇 ☞ 干货!学霸用12个决策模型告诉你,如何判断你到底需不需要区块链! ☞ 如何写出让同事无法维护的代码? eeb599eba3310cd08a665f8ca9639e4e.png 你点的每个“在看”,我都认真当成了喜欢
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值