Use React's functional components to implement a bar chart DIY component with transition changes, scale switching, and click highlighting

I wanted to use various open source chart libraries (such as: ECharts, G2 visualization engine, BizCharts...), but some requirements not only require transitional changes, but also click on a certain graph to highlight and send HTTP requests at the same time Data and other functions, I really don’t know how to highlight the charts drawn by canvas or svg, so I do it myself. Although React renders the view through the virtual DOM, it is best not to manipulate the DOM directly, but the current technology is limited, and it is only to manipulate the DOM to modify a little CSS style, which will be optimized later.

1. First design the parent page [/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. Then design subcomponents【/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. Finally, add some column chart style [/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. The effect is as follows:~

​​​​​​​

 

Guess you like

Origin blog.csdn.net/Cai181191/article/details/131640763
Recommended