使用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);
        }
      }
    }
  }
}

二、运行效果

目录
相关文章
|
26天前
|
前端开发 UED 索引
React 图片灯箱组件 Image Lightbox
图片灯箱组件是一种常见的Web交互模式,用户点击缩略图后弹出全屏窗口展示大图,并提供导航功能。本文介绍了基于React框架的图片灯箱组件开发,涵盖初始化状态管理、图片加载与预加载、键盘和鼠标事件处理等常见问题及解决方案。通过`useState`和`useEffect`钩子管理状态,使用懒加载和预加载优化性能,确保流畅的用户体验。代码案例展示了组件的基本功能实现,包括打开/关闭灯箱、切换图片及键盘操作。
125 80
|
10天前
|
移动开发 前端开发 JavaScript
React 视频播放控制组件 Video Controls
本文介绍了如何使用 React 构建视频播放控制组件(Video Controls),涵盖基本概念、创建步骤和常见问题解决。首先,通过 HTML5 `&lt;video&gt;` 标签和 React 组件化思想,实现播放/暂停按钮和进度条等基础功能。接着,详细讲解了初始化项目、构建 `VideoControls` 组件及与主应用的集成方法。最后,针对视频无法播放、控制器状态不同步、进度条卡顿和音量控制失效等问题提供了具体解决方案,并介绍了全屏播放和自定义样式等进阶功能。希望这些内容能帮助你在实际项目中更好地实现和优化视频播放功能。
81 40
|
25天前
|
移动开发 前端开发 JavaScript
React 视频播放器组件:Video Player
本文介绍了如何使用 React 和 HTML5 `&lt;video&gt;` 标签构建自定义视频播放器组件。首先,通过创建基础的 React 项目和 VideoPlayer 组件,实现了基本的播放、暂停功能。接着,探讨了常见问题如视频加载失败、控制条样式不一致、性能优化不足及状态管理混乱,并提供了相应的解决方案。最后,总结了构建高效视频播放器的关键要点,帮助开发者应对实际开发中的挑战。
107 27
|
28天前
|
前端开发 JavaScript API
React 图片放大组件 Image Zoom
本文介绍如何使用React创建图片放大组件(Image Zoom),提升用户体验。组件通过鼠标悬停或点击触发放大效果,利用`useState`管理状态,CSS实现视觉效果。常见问题包括图片失真、性能下降和移动端支持,分别可通过高质量图片源、优化事件处理和添加触摸事件解决。易错点涉及状态管理混乱、样式冲突和过多事件绑定,建议使用上下文API、CSS模块及优化事件绑定逻辑。高级功能扩展如多张图片支持和自定义放大区域进一步丰富了组件的实用性。
57 25
|
24天前
|
存储 编解码 前端开发
React 视频上传组件 Video Upload
随着互联网的发展,视频内容在网站和应用中愈发重要。本文探讨如何使用React构建高效、可靠的视频上传组件,涵盖基础概念、常见问题及解决方案。通过React的虚拟DOM和组件化开发模式,实现文件选择、进度显示、格式验证等功能,并解决跨域请求、并发上传等易错点。提供完整代码案例,确保用户能顺畅上传视频。
128 92
|
29天前
|
移动开发 前端开发 JavaScript
React 图片裁剪组件 Image Cropper
本文介绍了在React中实现图片裁剪功能的方法,涵盖基础知识、常见问题及解决方案。首先,通过第三方库如`react-image-crop`或`cropperjs-react`可轻松实现图片裁剪。接着,针对性能和兼容性问题,提供了优化图片加载、处理裁剪区域响应慢、解决浏览器差异等方案。最后,通过代码案例详细解释了如何创建一个基本的图片裁剪组件,并提出了优化建议,如使用`React.memo`、添加样式支持及处理大图片预览,帮助开发者避免常见错误并提升用户体验。
116 67
|
5天前
|
移动开发 前端开发 UED
React 音频预览组件:Audio Preview
本文介绍如何使用 React 构建音频预览组件,涵盖基础实现、常见问题及解决方案。通过 HTML5 `&lt;audio&gt;` 标签和 React 状态管理,实现播放控制。解决文件路径、浏览器兼容性等问题,并优化性能,避免状态不同步和内存泄漏,提升用户体验。
53 22
|
7天前
|
移动开发 前端开发 UED
React 音频播放器组件 Audio Player
本文介绍如何使用React创建功能丰富的音频播放器组件。基于HTML5 `&lt;audio&gt;` 标签,结合React的状态管理和事件处理,实现播放、暂停、进度和音量控制等功能。通过代码示例展示基本实现,并探讨常见问题如自动播放限制、进度条不更新、文件加载失败及多实例冲突的解决方案。同时,避免易错点如忽略生命周期管理、错误处理和性能优化,确保高效开发与良好用户体验。
61 23
|
9天前
|
缓存 前端开发 JavaScript
React 视频弹幕组件 Video Danmaku
本文介绍了如何在React中构建视频弹幕组件,提升用户观看体验和互动性。首先通过Create React App初始化项目,并集成`react-player`作为视频播放器。接着实现基本弹幕功能,包括评论的接收与显示,使用CSS动画实现滚动效果。针对常见问题如弹幕重叠、性能下降及同步问题,提供了随机化位置、分批加载和监听播放进度等解决方案。最后探讨了弹幕分类和特效等高级技巧,确保弹幕系统的高性能和良好用户体验。
50 23
|
27天前
|
存储 前端开发 索引
React 图片轮播组件 Image Carousel
本文介绍了如何使用React创建图片轮播组件。首先,解释了图片轮播的基本概念和组件结构,包括图片容器、导航按钮、指示器和自动播放功能。接着,通过代码示例详细说明了创建基本组件、添加自动播放、处理边界情况的步骤。还探讨了常见问题如状态更新不及时、内存泄漏和样式问题,并提供了解决方案。最后,介绍了进阶优化,如添加过渡效果、支持触摸事件和动态加载图片,帮助读者构建更完善的轮播组件。
47 16