本想使用业界大佬们开源的各种图表库(如: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) }} /><Buttontype=""size='small'style={{ fontSize: '13px', marginLeft: '7px' }}onClick={() => { handleQueryOnClick() }}>查询数据</Button></div><DiyBarChartref={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 = 0for (let vo of dataList) {barChartParams.totalNum += vo.num}// 3、设置刻度大小if (isOpenPercentage) {barChartParams.scaleSize = 100 // 若启用百分比刻度,则刻度大小为100} else {barChartParams.scaleSize = 0 // 若禁用百分比刻度,则刻度大小为数据列表中,最大数值的最接近的十倍数,且这个十倍数大于最大数值let maxSum = 0for (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.scaleGapfor (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 = heightvo.totalNum = barChartParams.totalNum} else {const height = divideToPercent(vo.num, barChartParams.scaleSize)vo.height = heightvo.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);}}}}
}
4、效果如下:~