React+Echarts实现数据排名+自动滚动+Y轴自定义toolTip文字提示
1、效果
2、环境准备
1、react18
2、antd 4+
3、代码实现
原理:自动滚动通过创建定时器动态更新echar的dataZoom属性startValue、endValue,自定义tooltip通过监听echar的鼠标移入移出事件,判断tooltTip元素的显隐以及位置。
1、导入所需组件:在你的代码文件中导入所需的组件
import ReactECharts from 'echarts-for-react';
2、创建一个定时器,处理当前图表滚动至第几个数据,用于实现自动滚动
// 开启定时器
const initialScroll = (dataLen: number, myChart: any) => {
const option = myChart.getOption();
// 只有当大于10条数据的时候 才会看起来滚动
let time = setInterval(() => {
if (data.length <= 8) {
return;
}
if (option?.dataZoom[0].endValue >= dataLen - 1) {
option.dataZoom[0].endValue = 7;
option.dataZoom[0].startValue = 0;
} else {
option.dataZoom[0].endValue = option.dataZoom[0].endValue + 1;
option.dataZoom[0].startValue = option.dataZoom[0].startValue + 1;
}
myChart.setOption(option);
myChart.setOption({
dataZoom: [
{
type: 'slider',
startValue: option.dataZoom[0].startValue,
endValue: option.dataZoom[0].endValue,
},
],
});
}, Number(rollTime));
timer = time;
};
3、在useEffect中添加自动滚动的定时器
useEffect(() => {
const myChart = chartRef?.current?.getEchartsInstance();
let chartDom = chartRef.current?.getEchartsInstance()?.getDom();
if (data.length > 8) {
initialScroll(data.length, myChart);
// 鼠标离开开启定时器
chartDom?.addEventListener('mouseout', () => {
if (timer) return;
initialScroll(data.length, myChart);
});
}
return () => {
clearInterval(timer);
timer = null;
};
}, [data]); // 检测数组内变量 如果为空 则监控全局
4、配置echars的属性,核心要配置dataZoom,控制数据从第几个开始展示,从第几个结束
export const getOption = (result) => {
return {
tooltip: {
...
},
},
grid: {
...
},
xAxis: [
{
...
},
],
yAxis: [
{
triggerEvent: true,
data: result.map((item) => item.projectName),
axisLabel: {
...
formatter: (value) => {
const valueNew =
value.length > 4 ? `${value.slice(0, 4)}...` : value;
const index = result.findIndex(
(item) => item.projectName === value,
);
if (index < 3) {
return `{icon${index + 1}|${index + 1}} {a|${valueNew}}`;
} else {
return `{icon0|${index + 1}} {a|${valueNew}}`;
}
}}
],
dataZoom: [
{
yAxisIndex: [0, 1], // 这里是从X轴的0刻度开始
show: false, // 是否显示滑动条,不影响使用
type: 'slider', // 这个 dataZoom 组件是 slider 型 dataZoom 组件
startValue: 0, // 从头开始。
endValue: 7, // 展示到第几个。
},
],
};
};
4、echarts Y轴的title超出会显示省略,但是看不全体验不好,于是给Y轴加了一个自定义的tooltTip,翻看的echarts所有属性,纵向坐标系,Y轴没有相关属性定义tooltTip
<ReactECharts
ref={chartRef}
option={getOption(data)}
className={clsx(['h-full w-full'])}
style={{ width: '100%', height: '100%' }}
/>
<div className="axis-tip"> </div>
于是在echarts同层节点添加一个toolTip节点,<div className="axis-tip"> </div> 就是ReactECharts的同层节点,通过监听echartsDom的鼠标移入移出控制toolTip的显示位置以及是否显示
useEffect(() => {
const myChart = chartRef?.current?.getEchartsInstance();
let chartDom = chartRef.current?.getEchartsInstance()?.getDom();
// 移入
chartDom?.addEventListener('mouseover', () => {
clearInterval(timer);
timer = undefined;
removeAxisTip();
});
// yAxis鼠标移入监听
myChart?.on?.('mouseover', 'yAxis.category', function (e: any) {
let axisTip: any = document.querySelector('.axis-tip');
if (axisTip) {
axisTip.innerText = e.value;
axisTip.style.left = (Number(e?.event?.event?.screenX) || 0) + 'px';
axisTip.style.top = (Number(e?.event?.event?.screenY) || 0) + 'px';
axisTip.style.display = 'block';
}
});
return () => {
myChart?.off('mouseover', () => {});
chartDom?.removeEventListener('mouseout', () => {});
chartDom?.removeEventListener('mouseover', () => {});
timer = null;
};
}, [data]); // 检测数组内变量 如果为空 则监控全局
5、完整代码如下:
/**
* 收集完成率排名 图表
*/
import clsx from 'clsx';
import ReactECharts from 'echarts-for-react';
import { useEffect, useRef } from 'react';
import '../index.less';
import { getOption } from './constants';
interface ProjectBarConfig {
data: any;
rollTime?: number;
}
const LineECharts = (props: ProjectBarConfig) => {
const { rollTime = 5000, data } = props;
const chartRef = useRef<ReactECharts>(null);
let timer: any = null;
// 开启定时器
const initialScroll = (dataLen: number, myChart: any) => {
const option = myChart.getOption();
// 只有当大于10条数据的时候 才会看起来滚动
let time = setInterval(() => {
if (data.length <= 8) {
return;
}
if (option?.dataZoom[0].endValue >= dataLen - 1) {
option.dataZoom[0].endValue = 7;
option.dataZoom[0].startValue = 0;
} else {
option.dataZoom[0].endValue = option.dataZoom[0].endValue + 1;
option.dataZoom[0].startValue = option.dataZoom[0].startValue + 1;
}
myChart.setOption(option);
myChart.setOption({
dataZoom: [
{
type: 'slider',
startValue: option.dataZoom[0].startValue,
endValue: option.dataZoom[0].endValue,
},
],
});
}, Number(rollTime));
timer = time;
};
// 移除y轴tip
const removeAxisTip = () => {
let axisTip: any = document.querySelector('.axis-tip');
if (axisTip) {
axisTip.innerText = '';
axisTip.style.display = 'none';
}
};
useEffect(() => {
const myChart = chartRef?.current?.getEchartsInstance();
let chartDom = chartRef.current?.getEchartsInstance()?.getDom();
if (data.length > 8) {
initialScroll(data.length, myChart);
// 鼠标离开开启定时器
chartDom?.addEventListener('mouseout', () => {
if (timer) return;
initialScroll(data.length, myChart);
});
}
// 移入
chartDom?.addEventListener('mouseover', () => {
clearInterval(timer);
timer = undefined;
removeAxisTip();
});
// yAxis鼠标移入监听
myChart?.on?.('mouseover', 'yAxis.category', function (e: any) {
let axisTip: any = document.querySelector('.axis-tip');
if (axisTip) {
axisTip.innerText = e.value;
axisTip.style.left = (Number(e?.event?.event?.screenX) || 0) + 'px';
axisTip.style.top = (Number(e?.event?.event?.screenY) || 0) + 'px';
axisTip.style.display = 'block';
}
});
// });
return () => {
clearInterval(timer);
myChart?.off('mouseover', () => {});
chartDom?.removeEventListener('mouseout', () => {});
chartDom?.removeEventListener('mouseover', () => {});
timer = null;
};
}, [data]); // 检测数组内变量 如果为空 则监控全局
const heightRate = Math.min((data.length || 1) / 8, 1) * 100;
return (
<div
className={clsx(['w-full h-full', 'flex flex-row'])}
onMouseLeave={() => {
removeAxisTip();
}}
>
<div
className={clsx(['flex-auto', 'h-full'])}
style={{
height: `${heightRate}%`,
maxHeight: '100%',
minHeight: '20%',
}}
>
<ReactECharts
ref={chartRef}
option={getOption(data)}
className={clsx(['h-full w-full'])}
style={{ width: '100%', height: '100%' }}
/>
<div className="axis-tip"> </div>
</div>
</div>
);
};
export default LineECharts;
echar属性配置:
const ranKIconList = [
'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyOCIgaGVpZ2h0PSIyMCIgdmlld0JveD0iMCAwIDI4IDIwIiBmaWxsPSJub25lIj4NCiAgPHBhdGggZD0iTTAgMEgyMEwyOCAxMC41TDIwIDIwSDBWMFoiIGZpbGw9InVybCgjcGFpbnQwX2xpbmVhcl8yNDIxM183MDA4KSIvPg0KICA8ZGVmcz4NCiAgICA8bGluZWFyR3JhZGllbnQgaWQ9InBhaW50MF9saW5lYXJfMjQyMTNfNzAwOCIgeDE9IjE0IiB5MT0iMCIgeDI9IjE0IiB5Mj0iMjAiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIj4NCiAgICAgIDxzdG9wIHN0b3AtY29sb3I9IiNGRkQxMkUiLz4NCiAgICAgIDxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iI0ZGQjgwMCIvPg0KICAgIDwvbGluZWFyR3JhZGllbnQ+DQogIDwvZGVmcz4NCjwvc3ZnPg==',
'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyOCIgaGVpZ2h0PSIyMSIgdmlld0JveD0iMCAwIDI4IDIxIiBmaWxsPSJub25lIj4NCiAgPHBhdGggZD0iTTAgMC4yODU2NDVIMjBMMjggMTAuNzg1NkwyMCAyMC4yODU2SDBWMC4yODU2NDVaIiBmaWxsPSJ1cmwoI3BhaW50MF9saW5lYXJfMjQyMTNfNzAxMSkiLz4NCiAgPGRlZnM+DQogICAgPGxpbmVhckdyYWRpZW50IGlkPSJwYWludDBfbGluZWFyXzI0MjEzXzcwMTEiIHgxPSIxNCIgeTE9IjAuMjg1NjQ1IiB4Mj0iMTQiIHkyPSIyMC4yODU2IiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+DQogICAgICA8c3RvcCBzdG9wLWNvbG9yPSIjQThDRkYwIi8+DQogICAgICA8c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9IiM4N0I4RTEiLz4NCiAgICA8L2xpbmVhckdyYWRpZW50Pg0KICA8L2RlZnM+DQo8L3N2Zz4=',
'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyOCIgaGVpZ2h0PSIyMSIgdmlld0JveD0iMCAwIDI4IDIxIiBmaWxsPSJub25lIj4NCiAgPHBhdGggZD0iTTAgMC41NzEyODlIMjBMMjggMTEuMDcxM0wyMCAyMC41NzEzSDBWMC41NzEyODlaIiBmaWxsPSJ1cmwoI3BhaW50MF9saW5lYXJfMjQyMTNfNzAxNCkiLz4NCiAgPGRlZnM+DQogICAgPGxpbmVhckdyYWRpZW50IGlkPSJwYWludDBfbGluZWFyXzI0MjEzXzcwMTQiIHgxPSIxNCIgeTE9IjAuNTcxMjg5IiB4Mj0iMTQiIHkyPSIyMC41NzEzIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+DQogICAgICA8c3RvcCBzdG9wLWNvbG9yPSIjRkFDNjgxIi8+DQogICAgICA8c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9IiNGNTkzMzgiLz4NCiAgICA8L2xpbmVhckdyYWRpZW50Pg0KICA8L2RlZnM+DQo8L3N2Zz4=',
'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyOCIgaGVpZ2h0PSIyMSIgdmlld0JveD0iMCAwIDI4IDIxIiBmaWxsPSJub25lIj4NCiAgPHBhdGggZD0iTTAgMC44NTY5MzRIMjBMMjggMTEuMzU2OUwyMCAyMC44NTY5SDBWMC44NTY5MzRaIiBmaWxsPSJ1cmwoI3BhaW50MF9saW5lYXJfMjQyMTNfNzAxNykiLz4NCiAgPGRlZnM+DQogICAgPGxpbmVhckdyYWRpZW50IGlkPSJwYWludDBfbGluZWFyXzI0MjEzXzcwMTciIHgxPSIxNCIgeTE9IjAuODU2OTM0IiB4Mj0iMTQiIHkyPSIyMC44NTY5IiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+DQogICAgICA8c3RvcCBzdG9wLWNvbG9yPSIjMEM0MjdDIi8+DQogICAgICA8c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9IiMwODM1NjYiLz4NCiAgICA8L2xpbmVhckdyYWRpZW50Pg0KICA8L2RlZnM+DQo8L3N2Zz4=',
];
// 配置调色板
export const colorPalette = [
['#2EF2FF', '#2EB3FF'],
['#FFD12E', '#FFB82E'],
['#8EED15', '#00CF61'],
['#CFCFCF', '#999'],
['#FF7D54', '#FF2E2E'],
['#00F3E5', '#00D4D6'],
].map(([startColor, endColor]) => ({
type: 'linear',
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [
{
offset: 0,
color: startColor, // 0% 处的颜色
},
{
offset: 1,
color: endColor, // 100% 处的颜色
},
],
global: false, // 缺省为 false
}));
export const getOption = (result) => {
return {
// color: '2379FF',
// backgroundColor: '#000416',
color: colorPalette,
tooltip: {
show: true,
trigger: 'axis',
padding: [8, 15],
backgroundColor: 'rgba(1, 15, 29, 80%)',
fontWeight: 700,
borderColor: 'rgba(46, 179, 255, 50%)',
textStyle: {
color: 'rgba(255, 255, 255, 1)',
},
},
legend: {
show: false,
},
grid: {
left: '100',
right: '52',
top: '0',
bottom: '4',
},
xAxis: [
{
splitLine: {
show: false,
},
type: 'value',
show: false,
axisLine: {
show: false,
},
},
],
yAxis: [
{
triggerEvent: true,
splitLine: {
show: false,
},
axisLine: {
show: false,
},
// type: 'category',
axisTick: {
show: false,
},
inverse: true,
// offset: 10,
data: result.map((item) => item.projectName),
axisLabel: {
color: '#fff',
fontSize: 12,
// marginLeft: 12,
overflow: 'truncate',
ellipsis: '...',
margin: 110,
align: 'left',
formatter: (value) => {
const valueNew =
value.length > 4 ? `${value.slice(0, 4)}...` : value;
const index = result.findIndex(
(item) => item.projectName === value,
);
if (index < 3) {
return `{icon${index + 1}|${index + 1}} {a|${valueNew}}`;
} else {
return `{icon0|${index + 1}} {a|${valueNew}}`;
}
},
rich: {
icon0: {
width: 28,
height: 18,
fontSize: 12,
align: 'center',
verticalAlign: 'middle',
color: '#fff',
padding: [3, 4], //[上, 右, 下, 左]
fontWeight: 400,
backgroundColor: {
image: ranKIconList[3],
},
},
icon1: {
width: 28,
height: 18,
fontSize: 12,
align: 'center',
verticalAlign: 'middle',
padding: [3, 4], //[上, 右, 下, 左]
backgroundColor: {
image: ranKIconList[0],
},
},
icon2: {
fontSize: 12,
align: 'center',
verticalAlign: 'middle',
padding: [3, 4], //[上, 右, 下, 左]
width: 28,
height: 18,
backgroundColor: {
// image: bg,
image: ranKIconList[1],
},
},
icon3: {
width: 28,
height: 18,
fontSize: 12,
verticalAlign: 'middle',
padding: [3, 4], //[上, 右, 下, 左]
align: 'center',
backgroundColor: {
image: ranKIconList[2],
},
},
a: {
fontSize: '12px',
color: '#B8D3F1',
align: 'center',
},
z: {
width: 6,
height: 6,
},
},
},
},
],
series: [
{
type: 'bar',
label: {
show: true,
position: 'right',
// distance: 0,
textStyle: {
fontSize: 12,
color: '#2EB3FF', // 值文字颜色
},
formatter: (value) => {
return `{a|${value?.data}%}`;
},
rich: {
a: {
fontSize: 12,
fontWeight: 500,
color: '#2EB3FF', // 值文字颜色
fontStyle: 'normal',
fontFamily: 'Arial',
padding: [0, 8, 0, 8], //[上, 右, 下, 左]
},
b: {
fontSize: 14,
},
},
},
itemStyle: {
normal: {
fontWeight: 400,
// color: function(params) {
// return barShadowColor[params.dataIndex]
// },
opacity: 0.8,
},
},
barWidth: 8,
data: result.map((item) => item.value),
// barGap:2,
z: 2,
},
{
name: '背景',
type: 'bar',
tooltip: { show: false },
barWidth: 12,
barHeight: 20,
barGap: '-100%',
// z: -1
},
],
dataZoom: [
{
yAxisIndex: [0, 1], // 这里是从X轴的0刻度开始
show: false, // 是否显示滑动条,不影响使用
type: 'slider', // 这个 dataZoom 组件是 slider 型 dataZoom 组件
startValue: 0, // 从头开始。
endValue: 7, // 展示到第几个。
},
],
};
};