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

本想使用业界大佬们开源的各种图表库(如: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 (
    <>
      
是否启用百分比刻度 : { setIsOpenPercentage(!isOpenPercentage) } } />
{ 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 (
    <>
      {/* ^ 柱状图 */}
      
    { y_AxisList.map((item, index) => { return (
  • { isOpenPercentage ? : }
  • ) }) }
    { x_AxisList.map((item, index) => { return (
  • handleBarChartOnClick(evt, item, index, x_AxisList.length)}> {

    { item.num } ({ divideToPercent(item.num, item.totalNum) })

    }
  • ) }) }
{/* / 柱状图 */} ) }) 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);
        }
      }
    }
  }
}

4、效果如下:~

​​​​​​​

 

你可能感兴趣的:(#,React,前端三大框架,前端大杂烩,react.js,前端)