前言
本想使用业界大佬们开源的各种图表库(如: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);
}
}
}
}
}