You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Grafana面板插件中实现共享十字线功能,并获取光标时间位置以自定义Tooltip?

我之前开发自定义Grafana面板插件时,正好碰到过和你一模一样的问题——想做带共享十字线的自定义Tooltip,却卡在了怎么精准获取光标时间对应的数值上,官方文档里确实没把这块讲透。后来我翻了Grafana的核心源码,还参考了社区里的一些案例,终于摸出了可行的方案,分享给你:

一、获取光标对应的时间戳

Grafana面板的时间范围和像素宽度是可以直接拿到的,我们可以通过光标在面板内的相对位置,换算出对应的时间戳:

  1. 首先监听面板的onMouseMove事件,拿到光标在页面上的clientX坐标;
  2. 获取面板元素的getBoundingClientRect(),计算光标相对于面板左侧的偏移量;
  3. 用面板的总时间范围(timeRange.to.unix() - timeRange.from.unix())除以面板宽度,得到每像素对应的时间跨度;
  4. 最后用起始时间加上偏移量乘以时间跨度,就能得到光标对应的时间戳。
二、匹配时间戳对应的数据值

拿到时间戳后,需要从你的数据集里找到最接近这个时间的数值,这里推荐用二分查找来提升效率(前提是你的数据已经按时间排序):

  • 写一个工具函数,通过二分法快速定位到目标时间附近的两个数据点;
  • 比较这两个点与目标时间的差值,取更接近的那个点的数值即可。
三、实现共享十字线与同步Tooltip

要让十字线在多个面板间同步,需要利用Grafana的事件总线(eventBus):

  1. 在当前面板触发光标移动事件时,通过eventBus.publish()广播当前的时间戳;
  2. 在其他自定义面板里,通过eventBus.subscribe()监听这个事件,拿到广播的时间戳后,重复前面的步骤匹配数值并更新Tooltip和十字线位置。
四、完整代码示例
import React, { useEffect, useState } from 'react';
import { PanelProps, PanelEvents } from '@grafana/ui';

const CustomTimeSeriesPanel = ({ data, timeRange, panel, eventBus }: PanelProps) => {
  const [cursorTime, setCursorTime] = useState<number | null>(null);
  const [tooltipContent, setTooltipContent] = useState<{ time: string; value: number } | null>(null);
  const [cursorXPos, setCursorXPos] = useState<number>(0);

  // 处理面板内的鼠标移动事件
  const handlePanelMouseMove = (e: React.MouseEvent<HTMLDivElement>) => {
    const panelEl = e.currentTarget;
    const { left, width } = panelEl.getBoundingClientRect();
    const relativeX = e.clientX - left;
    
    // 计算光标对应的时间戳
    const totalTimeSec = timeRange.to.unix() - timeRange.from.unix();
    const timeOffset = (relativeX / width) * totalTimeSec;
    const targetTime = timeRange.from.unix() + timeOffset;
    
    setCursorTime(targetTime);
    setCursorXPos((relativeX / width) * 100); // 转换为百分比用于定位十字线

    // 匹配对应数值
    const matchedValue = findClosestDataPoint(data, targetTime);
    if (matchedValue) {
      setTooltipContent({
        time: new Date(targetTime * 1000).toLocaleString(),
        value: matchedValue.value
      });
    }

    // 广播光标时间,实现跨面板共享
    eventBus.publish(PanelEvents.pointerMove, { time: targetTime });
  };

  // 监听其他面板的光标同步事件
  useEffect(() => {
    const subscription = eventBus.subscribe(PanelEvents.pointerMove, (event) => {
      if (event.time && typeof event.time === 'number') {
        setCursorTime(event.time);
        // 计算同步后的光标X位置百分比
        const totalTimeSec = timeRange.to.unix() - timeRange.from.unix();
        const timeRatio = (event.time - timeRange.from.unix()) / totalTimeSec;
        setCursorXPos(timeRatio * 100);
        
        const matchedValue = findClosestDataPoint(data, event.time);
        if (matchedValue) {
          setTooltipContent({
            time: new Date(event.time * 1000).toLocaleString(),
            value: matchedValue.value
          });
        }
      }
    });

    return () => subscription.unsubscribe();
  }, [eventBus, data, timeRange]);

  // 二分查找最接近目标时间的数据点
  const findClosestDataPoint = (data: any[], targetTime: number) => {
    if (!data || data.length === 0) return null;
    const sortedData = data[0].fields.find((f: any) => f.name === 'time').values.toArray();
    const valueData = data[0].fields.find((f: any) => f.type === 'number').values.toArray();

    let left = 0;
    let right = sortedData.length - 1;

    while (left < right) {
      const mid = Math.floor((left + right) / 2);
      if (sortedData[mid] < targetTime) {
        left = mid + 1;
      } else {
        right = mid;
      }
    }

    // 比较左右两个点哪个更接近目标时间
    const prevIndex = left > 0 ? left - 1 : left;
    const currDiff = Math.abs(sortedData[left] - targetTime);
    const prevDiff = Math.abs(sortedData[prevIndex] - targetTime);

    return currDiff < prevDiff 
      ? { time: sortedData[left], value: valueData[left] } 
      : { time: sortedData[prevIndex], value: valueData[prevIndex] };
  };

  return (
    <div 
      id={`panel-${panel.id}`}
      onMouseMove={handlePanelMouseMove}
      onMouseLeave={() => {
        setCursorTime(null);
        setTooltipContent(null);
      }}
      style={{ position: 'relative', width: '100%', height: '100%' }}
    >
      {/* 渲染你的图表内容 */}
      {/* 共享十字线 */}
      {cursorTime && (
        <div 
          style={{
            position: 'absolute',
            left: `${cursorXPos}%`,
            top: 0,
            bottom: 0,
            width: 1,
            backgroundColor: '#ff4444',
            opacity: 0.6,
            pointerEvents: 'none'
          }}
        />
      )}
      {/* 自定义Tooltip */}
      {tooltipContent && (
        <div 
          style={{
            position: 'absolute',
            left: `${cursorXPos + 2}%`,
            top: '10px',
            background: '#ffffff',
            padding: '8px 12px',
            borderRadius: '4px',
            boxShadow: '0 2px 8px rgba(0,0,0,0.15)',
            pointerEvents: 'none',
            zIndex: 100
          }}
        >
          <div><strong>时间:</strong> {tooltipContent.time}</div>
          <div><strong>数值:</strong> {tooltipContent.value.toFixed(2)}</div>
        </div>
      )}
    </div>
  );
};

export default CustomTimeSeriesPanel;
五、注意事项
  • 确保你的数据集是按时间正序排列的,否则二分查找会失效;
  • 如果是多系列数据,需要遍历每个系列分别匹配数值,再在Tooltip里展示所有系列的内容;
  • 记得处理鼠标离开面板的事件,隐藏十字线和Tooltip;
  • 跨面板共享时,要确保所有自定义面板都监听同一个PanelEvents.pointerMove事件。

内容的提问来源于stack exchange,提问作者Leonardo Pivetta

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.30 20:32:35