使用React的函数式组件实现一个具有过渡变化、刻度切换、点击高亮的柱状图DIY组件

简介: 本文展示了如何使用React的函数式组件实现一个具有过渡变化、刻度切换、点击高亮效果的自定义柱状图组件,并提供了完整的示例代码和实现效果。

前言

本想使用业界大佬们开源的各种图表库(如:ECharts、G2可视化引擎、BizCharts ...),但是有的需求不仅要求有过渡变化,还要点击某个图高亮同时发送HTTP请求数据等功能,着实不知道怎么把canvas或svg绘制的图表弄成高亮,于是自己动手丰衣足食。虽然说React是通过虚拟DOM来渲染视图的,最好不要直接操作DOM,但是目前技术有限,而且也只是操作一下DOM来修改一点点CSS样式,这个以后再优化吧。

一、示例代码

(1)首先设计父页面【/src/views/Example/DiyCharts/index.jsx】

import {
   
    Button, Switch } from 'antd'
import {
   
    useEffect, useState, useRef } from 'react'
import DiyBarChart from './components/diyBarChart'

const DiyCharts = () => {
   
   

  // 柱状图引用对象
  const diyBarChartRef = useRef(null)

  // 柱状图数据列表
  const [dataList, setDataList] = useState([])

  // 是否启用百分比刻度,若启用则显示百分比,若禁用则显示具体数值
  const [isOpenPercentage, setIsOpenPercentage] = useState(false)

  /**
   * 查询事件句柄
   */
  const handleQueryOnClick = function () {
   
   
    diyBarChartRef.current.handleResetBarChar()
    setTimeout(() => {
   
   
      setDataList(
        [
          {
   
    num: (Math.floor(Math.random() * 100)), title: '家具家电' },
          {
   
    num: (Math.floor(Math.random() * 100)), title: '生鲜水果' },
          {
   
    num: (Math.floor(Math.random() * 100)), title: '粮油副食' },
          {
   
    num: (Math.floor(Math.random() * 100)), title: '母婴用品' },
          {
   
    num: (Math.floor(Math.random() * 100)), title: '美容护肤' },
          {
   
    num: (Math.floor(Math.random() * 100)), title: '清洁卫生' },
        ]
      )
    }, 1500)
  }

  useEffect(() => {
   
   
    handleQueryOnClick()
  }, [])

  return (
    <>
      <div style={
   
   {
   
    display: 'flex', alignItems: 'center' }}>
        <span style={
   
   {
   
    fontSize: '13px', margin: '7px' }}>是否启用百分比刻度 : </span>

        <Switch checked={
   
   isOpenPercentage} onChange={
   
   
          () => {
   
    setIsOpenPercentage(!isOpenPercentage) }
        } />

        <Button
          type=""
          size='small'
          style={
   
   {
   
    fontSize: '13px', marginLeft: '7px'  }}
          onClick={
   
   
            () => {
   
    handleQueryOnClick() }
          }>
          查询数据
        </Button>
      </div>

      <DiyBarChart
        ref={
   
   diyBarChartRef}
        width={
   
    '450px' }
        height={
   
    '300px' }
        dataList={
   
   dataList}
        isOpenPercentage={
   
   isOpenPercentage}
        onData={
   
   
          (item) => {
   
   
            console.log(item)
          }
        } 
      />
    </>
  )
}

export default DiyCharts

(2)然后设计子组件【/src/views/Example/DiyCharts/components/diyBarChart/index.jsx】

import {
   
    useState, useEffect, useRef, forwardRef, useImperativeHandle } from 'react'
import {
   
    message } from 'antd'
import './style.scss'

const DiyBarChart = forwardRef((props, ref) => {
   
   

  const barChartRef = useRef(null)

  const {
   
    width, height, dataList, isOpenPercentage } = props

  // 柱状图配置参数
  let barChartParams = {
   
   
    width: width ? width : '600px',
    height: height ? height : '150px',
    scaleSize: 0, // 刻度大小
    scaleGap: 5, // 刻度间隔
    totalNum: 0, // 数值总数
    barIdPrefix: 'diy-bar-chart-', // 柱状图li元素的ID前缀,如:diy-bar-chart-0 diy-bar-chart-1 diy-bar-chart-2 diy-bar-chart-3
  }

  // 柱状图y轴刻度列表
  const [y_AxisList, setY_AxisList] = useState(
    [100, 80, 60, 40, 20, 0]
  )

  // 柱状图x轴数据列表
  const [x_AxisList, setX_AxisList] = useState(
    [
      {
   
    'num': 0, title: '家具家电', height: '0%', totalNum: 1 },
      {
   
    'num': 0, title: '生鲜水果', height: '0%', totalNum: 1 },
      {
   
    'num': 0, title: '粮油副食', height: '0%', totalNum: 1 },
      {
   
    'num': 0, title: '母婴用品', height: '0%', totalNum: 1 },
      {
   
    'num': 0, title: '美容护肤', height: '0%', totalNum: 1 },
      {
   
    'num': 0, title: '清洁卫生', height: '0%', totalNum: 1 },
    ]
  )

  /**
   * 两数相除结果转为百分数
   */
  const divideToPercent = (num1, num2) => {
   
   
    return (Math.round(num1 / num2 * 10000) / 100.00 + '%')
  }

  /**
   * 获取一个数且大于它,以及与它最接近的十倍数
   */
  const getNearestTen = (num) => {
   
   
    return Math.ceil(num/10) * 10
  }

  /**
   * 构建柱状图数据
   */
  const handleInitBarChart = async (dataList) => {
   
   
    if (dataList.length == 0) {
   
   
      return
    }

    try {
   
   
      console.log('dataList =>', dataList)

      // 2、设置数值总数
      barChartParams.totalNum = 0
      for (let vo of dataList) {
   
   
        barChartParams.totalNum += vo.num
      }

      // 3、设置刻度大小
      if (isOpenPercentage) {
   
   
        barChartParams.scaleSize = 100 // 若启用百分比刻度,则刻度大小为100
      } else {
   
   
        barChartParams.scaleSize = 0 // 若禁用百分比刻度,则刻度大小为数据列表中,最大数值的最接近的十倍数,且这个十倍数大于最大数值
        let maxSum = 0
        for (let vo of dataList) {
   
   
          if (vo.num > maxSum) {
   
   
            maxSum = vo.num
          }
        }
        barChartParams.scaleSize = getNearestTen(maxSum)
      }

      // 4、设置柱状图y轴刻度列表
      const tempY_AxisList = []
      const degree = barChartParams.scaleSize / barChartParams.scaleGap
      for (let i = 0; i <= barChartParams.scaleGap; i++) {
   
   
        tempY_AxisList.push(parseInt(i * degree))
      }
      tempY_AxisList.sort(
        (a, b) => {
   
   
          return b - a // 倒序
        }
      )
      setY_AxisList(tempY_AxisList)
      // console.log('tempY_AxisList =>', tempY_AxisList)

      // 5、设置柱状图x轴数据列表
      const tempX_AxisList = []
      for (let vo of dataList) {
   
   
        if (isOpenPercentage) {
   
   
          const height = divideToPercent(vo.num, barChartParams.totalNum)
          vo.height = height
          vo.totalNum = barChartParams.totalNum
        } else {
   
   
          const height = divideToPercent(vo.num, barChartParams.scaleSize)
          vo.height = height
          vo.totalNum = barChartParams.totalNum
        }
        tempX_AxisList.push(vo)
      }
      setX_AxisList(tempX_AxisList)
      // console.log('tempX_AxisList =>', tempX_AxisList)
    } catch (e) {
   
   
      console.error(e)
    }
  }

  /**
  * 柱状图点击事件句柄方法
  */
  const handleBarChartOnClick = async (evt, item, index, length) => {
   
   
    console.log('handleBarChartOnClick =>', evt, item, index, length)
    message.info(JSON.stringify(item), 1)

    const current = await barChartRef.current
    // console.log('barChartRef.current =>', current)

    for (let i = 0; i < length; i++) {
   
   
      const li = document.getElementById(barChartParams.barIdPrefix + i)
      li.querySelector('div').style.backgroundColor = 'transparent'
    }

    const li = document.getElementById(barChartParams.barIdPrefix + index)
    li.querySelector('div').style.backgroundColor = 'rgba(199, 220, 255, 0.8)'

    props.onData(item) // 子组件传参给父页面
  }

  const handleResetBarChar = () => {
   
   
    setX_AxisList(
      [
        {
   
    'num': 0, title: '家具家电', height: '0%', totalNum: 1 },
        {
   
    'num': 0, title: '生鲜水果', height: '0%', totalNum: 1 },
        {
   
    'num': 0, title: '粮油副食', height: '0%', totalNum: 1 },
        {
   
    'num': 0, title: '母婴用品', height: '0%', totalNum: 1 },
        {
   
    'num': 0, title: '美容护肤', height: '0%', totalNum: 1 },
        {
   
    'num': 0, title: '清洁卫生', height: '0%', totalNum: 1 },
      ]
    )
  }

  /**
   * 将子组件的方法暴露给父组件调用
   */
  useImperativeHandle(ref, () => ({
   
   
    handleResetBarChar
  }))

  useEffect(() => {
   
   
    console.log('dataList =>', dataList)
    handleInitBarChart(dataList)
  }, [dataList, isOpenPercentage])

  return (
    <>
      {
   
   /* ^ 柱状图 */}
      <div ref={
   
   barChartRef} className="diy-bar-chart" style={
   
   {
   
    width: barChartParams.width, height: barChartParams.height }}>
        <div className="diy-bar-chart__container">

            <div className="__y-axis" />

            <ul className="__y-ul">
            {
   
   
                y_AxisList.map((item, index) => {
   
   
                return (
                    <li key={
   
   index}>
                    {
   
   
                        isOpenPercentage
                        ? <span><label>{
   
    item + '%'}</label></span>
                        : <span><label>{
   
    item }</label></span>
                    }
                    </li>
                )
                })
            }
            </ul>

            <ul className="__x-ul">
            {
   
   
                x_AxisList.map((item, index) => {
   
   
                    return (
                    <li id={
   
   barChartParams.barIdPrefix + index} key={
   
   index} onClick={
   
   evt => handleBarChartOnClick(evt, item, index, x_AxisList.length)}>
                        {
   
   
                        <div className="__bar-outer">
                            <div className="__bar-inner" style={
   
   {
   
    height: item.height }} data-height={
   
    item.height }>
                              <p>
                                  <span>{
   
    item.num }</span>
                                  <small>({
   
    divideToPercent(item.num, item.totalNum) })</small>
                              </p>
                            </div>
                            <label>{
   
    item.title }</label>
                        </div>
                        }
                    </li>
                    )
                })
            }
            </ul>
        </div>
      </div>
      {
   
   /* / 柱状图 */}
    </>
  )
})

export default DiyBarChart

(3)最后加点柱状图样式【/src/views/Example/DiyCharts/components/diyBarChart/style.scss】

.diy-bar-chart {
   
   
  position: relative;
  display: table;
  padding: 35px 0 25px 50px;
  transition: all ease 0.3s;

  .diy-bar-chart__container {
   
   
    position: relative;
    display: flex;
    flex-direction: row;
    width: 100%;
    height: 100%;
    margin: 0 auto;

    .__y-axis {
   
   
      position: absolute;
      bottom: 0;
      width: 1px;
      height: calc(100% + 35px);
      border-left: 1px solid #ddd;
    }

    .__y-ul {
   
   
      position: absolute;
      display: flex;
      flex-direction: column;
      width: 100%;
      height: 100%;
      margin: 0;
      padding: 0;

      li {
   
   
        position: relative;
        bottom: 0;
        flex: 1;
        display: flex;
        border-top: 1px solid #ddd;
        list-style: none;

        span {
   
   
          position: absolute;
          bottom: 0;
          left: -45px;
          top: -50%;
          display: block;
          width: 35px;
          height: 100%;
          text-align: right;

          label {
   
   
            position: absolute;
            display: grid;
            width: 100%;
            height: 100%;
            align-items: center;
            font-size: 13px;
            text-align: right;
            color: #686868;
          }
        }
      }

      li:last-child {
   
   
        flex: 0;

        span {
   
   
          top: -6.5px;
        }
      }

      &:before {
   
   
        position: relative;
        bottom: 35px;
        font-size: 13px;
        color: #5e7ce0;
        border-left: 1px solid #f00;
      }
    }

    .__x-ul {
   
   
      display: flex;
      width: 100%;
      height: 100%;
      margin: 0;
      padding: 0 10px;

      li {
   
   
        display: table-cell;
        flex: 1;
        height: 100%;      
        text-align: center;
        position: relative;

        .__bar-outer {
   
   
          position: relative;
          width: 100%;
          height: 100%;
          transition: all ease 0.3s;
          cursor: pointer;


          .__bar-inner {
   
   
            position: absolute;
            bottom: 0;
            left: 0;
            right: 0;
            display: block;
            margin: 0 auto;
            width: 20px;
            height: 0;
            background-color: #5e7ce0;
            transition: all ease-in-out 0.3s;
            text-align: center;

            p {
   
   
              position: relative;
              left: 0;
              bottom: 32px;
              width: 100px;
              height: 100%;
              transform: translateX(-40px);
              margin: 0;
              font-size: 13px;
              color: #5e7ce0;
              text-align: center;

              span {
   
   
                display: block;
                font-size: 14px;
                line-height: 14px;
              }

              small {
   
   
                font-size: 12px;
                line-height: 12px;
                color: #686868;
              }
            }
          }

          label {
   
   
            position: absolute;
            left: 0;
            bottom: -25px;
            width: 100%;
            text-align: center;
            font-size: 13px;
            color: #686868;
          }

          &:hover {
   
   
            background-color: rgb(231, 240, 255, 0.8) !important;
          }
        }
      }

      li:first-child {
   
   

        .__bar-outer {
   
   
          background-color: rgba(199, 220, 255, 0.8);
        }
      }
    }
  }
}

二、运行效果

目录
相关文章
|
1月前
|
缓存 前端开发 JavaScript
React Hooks深度解析与最佳实践:提升函数组件能力的终极指南
🌟蒋星熠Jaxonic,前端探索者。专注React Hooks深度实践,从原理到实战,分享状态管理、性能优化与自定义Hook精髓。助力开发者掌握函数组件的无限可能,共赴技术星辰大海!
React Hooks深度解析与最佳实践:提升函数组件能力的终极指南
|
6月前
|
缓存 前端开发 数据安全/隐私保护
如何使用组合组件和高阶组件实现复杂的 React 应用程序?
如何使用组合组件和高阶组件实现复杂的 React 应用程序?
270 68
|
6月前
|
缓存 前端开发 Java
在 React 中,组合组件和高阶组件在性能方面有何区别?
在 React 中,组合组件和高阶组件在性能方面有何区别?
247 67
|
6月前
|
前端开发 JavaScript 安全
除了高阶组件和render props,还有哪些在 React 中实现代码复用的方法?
除了高阶组件和render props,还有哪些在 React 中实现代码复用的方法?
272 62
|
8月前
|
前端开发
React 中高阶组件的原理是什么?
React 中高阶组件的原理是什么?
231 57
|
8月前
|
移动开发 前端开发 JavaScript
React音频播放列表组件:常见问题、易错点与解决方案
本文介绍了在React中实现音频播放列表时常见的挑战及解决方案。通过基础实现、常见问题分析和最佳实践,帮助开发者避免状态管理、生命周期控制和事件处理中的陷阱。关键点包括使用`useRef`操作音频元素、`useState`同步播放状态、全局状态管理防止多音频同时播放、以及通过`useEffect`清理资源。还提供了代码示例和跨浏览器兼容性处理方法,确保高效实现功能并减少调试时间。
280 30
|
8月前
|
移动开发 前端开发 UED
React 音频音量控制组件 Audio Volume Control
在现代Web应用中,音频播放功能不可或缺。React以其声明式编程和组件化开发模式,非常适合构建复杂的音频音量控制组件。本文介绍了如何使用HTML5 `&lt;audio&gt;`元素与React结合,实现直观的音量控制系统,并解决了常见问题如音量范围不合理、初始音量设置及性能优化等,帮助开发者打造优秀的音频播放器。
288 27
|
8月前
|
编解码 前端开发 开发者
React 图片组件样式自定义:常见问题与解决方案
在 React 开发中,图片组件的样式自定义常因细节问题导致布局错乱、性能损耗或交互异常。本文系统梳理常见问题及解决方案,涵盖基础样式应用、响应式设计、加载状态与性能优化等,结合代码案例帮助开发者高效实现图片组件的样式控制。重点解决图片尺寸不匹配、边框阴影不一致、移动端显示模糊、加载失败处理及懒加载等问题,并总结易错点和最佳实践,助力开发者提升开发效率和用户体验。
265 22
|
8月前
|
移动开发 前端开发 开发者
React 音频播放控制组件 Audio Controls
本文介绍了如何使用React构建音频播放控制组件,涵盖HTML5 `&lt;audio&gt;`标签和React组件化思想的基础知识。针对常见问题如播放状态管理、进度条更新不准确及跨浏览器兼容性,提供了详细的解决方案和代码示例。同时,还总结了易错点及避免方法,如确保音频加载完成再操作、处理音频错误等,帮助开发者实现稳定且功能强大的音频播放器。
347 11
|
8月前
|
移动开发 前端开发 UED
React 音频进度条组件 Audio Progress Bar
在现代Web开发中,音频播放功能不可或缺。使用React构建音频进度条组件,不仅能实现播放控制和拖动跳转,还能确保代码的可维护性和复用性。本文介绍了如何利用HTML5 `&lt;audio&gt;`标签的基础功能、解决获取音频时长和当前时间的问题、动态更新进度条样式,并避免常见错误如忘记移除事件监听器和忽略跨浏览器兼容性。通过这些方法,开发者可以打造高质量的音频播放器,提升用户体验。
309 6