We read every piece of feedback, and take your input very seriously.
To see all available qualifiers, see our documentation.
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
某天在逛社区时看到一帖子:
react-dynamic-charts — A React Library for Visualizing Dynamic Data
这是一个国外大佬在其公司峰会的代码竞赛中写的一个库:react-dynamic-charts,用于根据动态数据创建动态图表可视化。 它的设计非常灵活,允许你控制内部的每个元素和事件。使用方法也非常简单,其源码也是非常精炼,值得学习。
react-dynamic-charts
但因其提供了不少API,不利于理解源码。所以以下实现有所精简:
API
getRandomColor
const getRandomColor = () => { const letters = '0123456789ABCDEF'; let color = '#'; for (let i = 0; i < 6; i++) { color += letters[Math.floor(Math.random() * 16)] } return color; };
translateY
const translateY = (value) => { return `translateY(${value}px)`; }
useState Hook
我们开始编写组件DynamicBarChart
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); // 其它代码... }
useState
const [属性, 操作属性的方法] = useState(默认值);
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 可选的清除机制。每个 effect 都可以返回一个清除函数。如此可以将添加和移除订阅的逻辑放在一起。
Dom
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
label
bar
value
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} /> ) } }
Mock
helpers.js:
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:
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); 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:
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
一直对实现动态排行榜可视化感兴趣,无奈多数都是基于D3或echarts实现。 而这个库,不仅脱离图形库,还使用了React 16的新特性。也让我彻底理解了React Hook的妙用。
D3
echarts
React 16
React Hook
如果你觉得这篇内容对你挺有启发,我想邀请你帮我三个小忙:
懒得clone项目的可以公众号后台回复「可视化」,直接拿核心代码,拖进项目用。
clone
The text was updated successfully, but these errors were encountered:
No branches or pull requests
前言
某天在逛社区时看到一帖子:
这是一个国外大佬在其公司峰会的代码竞赛中写的一个库:
react-dynamic-charts
,用于根据动态数据创建动态图表可视化。它的设计非常灵活,允许你控制内部的每个元素和事件。使用方法也非常简单,其源码也是非常精炼,值得学习。
但因其提供了不少
API
,不利于理解源码。所以以下实现有所精简:1. 准备通用工具函数
1.
getRandomColor
:随机颜色2.
translateY
:填充Y轴偏移量2. 使用
useState Hook
声明状态变量我们开始编写组件
DynamicBarChart
1.
useState
的简单理解:2. 变量解析
dataQueue
:当前操作的原始数据数组activeItemIdx
: 第几“帧”highestValue
: “榜首”的数据值currentValues
: 经过处理后用于渲染的数据数组firstRun
: 第一次动态渲染时间3. 内部操作方法和对应
useEffect
请配合注释食用
对应
useEffect
:useEffect
示例:为什么要在
effect
中返回一个函数?这是
effect
可选的清除机制。每个effect
都可以返回一个清除函数。如此可以将添加和移除订阅的逻辑放在一起。4. 整理用于渲染
Dom
的数据keys
: 每组数据的索引maxValue
: 图表最大宽度sortedCurrentValues
: 对每组数据进行排序,该项影响动态渲染。currentItem
: 每组的原始数据5. 开始渲染...
大致的逻辑就是:
Props
,循环排列后的数据:sortedCurrentValues
label
、bar
、value
transform
。6. 定义常规
propTypes
和defaultProps
:7. 如何使用
1. 批量生成
Mock
数据helpers.js
:mocks.js
:一个乞丐版的动态排行榜可视化就做好喇。
8. 完整代码
styles.scss
:原项目地址:react-dynamic-charts
结语
一直对实现动态排行榜可视化感兴趣,无奈多数都是基于
D3
或echarts
实现。而这个库,不仅脱离图形库,还使用了
React 16
的新特性。也让我彻底理解了React Hook
的妙用。❤️ 看完三件事
如果你觉得这篇内容对你挺有启发,我想邀请你帮我三个小忙:
懒得
clone
项目的可以公众号后台回复「可视化」,直接拿核心代码,拖进项目用。The text was updated successfully, but these errors were encountered: