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

二、运行效果

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值