React+Echarts实现数据排名+自动滚动+Y轴自定义toolTip文字提示

1、效果

2、环境准备

1、react18

2、antd 4+

3、代码实现

原理:自动滚动通过创建定时器动态更新echar的dataZoom属性startValue、endValue,自定义tooltip通过监听echar的鼠标移入移出事件,判断tooltTip元素的显隐以及位置。

1、导入所需组件:在你的代码文件中导入所需的组件

javascript 复制代码
import ReactECharts from 'echarts-for-react';

2、创建一个定时器,处理当前图表滚动至第几个数据,用于实现自动滚动

javascript 复制代码
 // 开启定时器
  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中添加自动滚动的定时器

javascript 复制代码
  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,控制数据从第几个开始展示,从第几个结束

javascript 复制代码
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

html 复制代码
  <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的显示位置以及是否显示

javascript 复制代码
  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、完整代码如下:

javascript 复制代码
/**
 * 收集完成率排名 图表
 */
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属性配置:

javascript 复制代码
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, // 展示到第几个。
      },
    ],
  };
};
相关推荐
我是哈哈hh几秒前
HTML5和CSS3的进阶_HTML5和CSS3的新增特性
开发语言·前端·css·html·css3·html5·web
田本初18 分钟前
如何修改npm包
前端·npm·node.js
明辉光焱39 分钟前
[Electron]总结:如何创建Electron+Element Plus的项目
前端·javascript·electron
牧码岛1 小时前
Web前端之汉字排序、sort与localeCompare的介绍、编码顺序与字典顺序的区别
前端·javascript·web·web前端
开心工作室_kaic1 小时前
ssm111基于MVC的舞蹈网站的设计与实现+vue(论文+源码)_kaic
前端·vue.js·mvc
云空2 小时前
《InsCode AI IDE:编程新时代的引领者》
java·javascript·c++·ide·人工智能·python·php
晨曦_子画2 小时前
用于在 .NET 中构建 Web API 的 FastEndpoints 入门
前端·.net
慧都小妮子2 小时前
Spire.PDF for .NET【页面设置】演示:在 PDF 文件中添加图像作为页面背景
前端·pdf·.net·spire.pdf
咔咔库奇2 小时前
ES6基础
前端·javascript·es6