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

二、运行效果

目录
相关文章
|
7天前
|
前端开发 JavaScript 开发者
React 按钮组件 Button
本文介绍了 React 中按钮组件的基础概念,包括基本的 `&lt;button&gt;` 元素和自定义组件。详细探讨了事件处理、参数传递、状态管理、样式设置和可访问性优化等常见问题及其解决方案,并提供了代码示例。帮助开发者避免易错点,提升按钮组件的使用体验。
114 77
|
4天前
|
存储 前端开发 UED
React 面包屑组件 Breadcrumb 详解
面包屑导航是现代Web应用中常见的UI元素,帮助用户了解当前位置并快速返回上级页面。本文介绍如何使用React构建面包屑组件,涵盖基本概念、实现方法及常见问题。通过函数式组件和钩子,结合React Router动态生成路径,处理嵌套路由,并确保可访问性。示例代码展示了静态和动态面包屑的实现,帮助开发者提升用户体验。
99 73
|
8天前
|
前端开发 UED 开发者
React 对话框组件 Dialog
本文详细介绍了如何在 React 中实现一个功能完备的对话框组件(Dialog),包括基本用法、常见问题及其解决方案,并通过代码案例进行说明。从安装依赖到创建组件、添加样式,再到解决关闭按钮失效、背景点击无效、键盘导航等问题,最后还介绍了如何添加动画效果和处理异步关闭操作。希望本文能帮助你在实际开发中更高效地使用 React 对话框组件。
111 75
|
13天前
|
前端开发 Java API
React 进度条组件 ProgressBar 详解
本文介绍了如何在 React 中创建进度条组件,从基础实现到常见问题及解决方案,包括动态更新、状态管理、性能优化、高级动画效果和响应式设计等方面,帮助开发者构建高效且用户体验良好的进度条。
39 18
|
27天前
|
存储 前端开发 测试技术
React组件的最佳实践
React组件的最佳实践
|
26天前
|
前端开发 API 开发者
React 文件上传组件 File Upload
本文详细介绍了如何在 React 中实现文件上传组件,从基础的文件选择和上传到服务器,再到解决文件大小、类型限制、并发上传等问题,以及实现多文件上传、断点续传和文件预览等高级功能,帮助开发者高效构建可靠的应用。
52 12
|
21天前
|
存储 前端开发 JavaScript
React 表单输入组件 Input:常见问题、易错点及解决方案
本文介绍了在 React 中使用表单输入组件 `Input` 的基础概念,包括受控组件与非受控组件的区别及其优势。通过具体代码案例,详细探讨了创建受控组件、处理多个输入字段、输入验证和格式化的方法,并指出了常见易错点及避免方法,旨在提升表单的健壮性和用户体验。
33 4
|
28天前
|
前端开发 JavaScript API
React 文件下载组件 File Download
本文介绍了在React中实现文件下载组件的方法,包括使用`a`标签和JavaScript动态生成文件,解决了文件路径、文件类型、大文件下载及文件名乱码等问题,并展示了使用第三方库`file-saver`和生成CSV文件的高级用法。
38 6
|
25天前
|
前端开发 JavaScript API
React 文件下载组件:File Download
本文详细介绍了如何在React应用中实现文件下载组件,包括基本概念、实现步骤和代码示例。同时,探讨了常见问题如文件类型不匹配、文件名乱码等及其解决方法,旨在提升用户体验和代码可维护性。
43 2
|
29天前
|
存储 前端开发 JavaScript
React 文件上传组件 File Upload
本文介绍了如何在 React 中实现文件上传组件,包括基本的概念、实现步骤、常见问题及解决方案。通过 `&lt;input type=&quot;file&quot;&gt;` 元素选择文件,使用 `fetch` 发送请求,处理文件类型和大小限制,以及多文件上传和进度条显示等高级功能,帮助开发者构建高效、可靠的文件上传组件。
90 2