如何在Grafana面板插件中实现共享十字线功能,并获取光标时间位置以自定义Tooltip?
我之前开发自定义Grafana面板插件时,正好碰到过和你一模一样的问题——想做带共享十字线的自定义Tooltip,却卡在了怎么精准获取光标时间对应的数值上,官方文档里确实没把这块讲透。后来我翻了Grafana的核心源码,还参考了社区里的一些案例,终于摸出了可行的方案,分享给你:
一、获取光标对应的时间戳
Grafana面板的时间范围和像素宽度是可以直接拿到的,我们可以通过光标在面板内的相对位置,换算出对应的时间戳:
- 首先监听面板的
onMouseMove事件,拿到光标在页面上的clientX坐标; - 获取面板元素的
getBoundingClientRect(),计算光标相对于面板左侧的偏移量; - 用面板的总时间范围(
timeRange.to.unix() - timeRange.from.unix())除以面板宽度,得到每像素对应的时间跨度; - 最后用起始时间加上偏移量乘以时间跨度,就能得到光标对应的时间戳。
二、匹配时间戳对应的数据值
拿到时间戳后,需要从你的数据集里找到最接近这个时间的数值,这里推荐用二分查找来提升效率(前提是你的数据已经按时间排序):
- 写一个工具函数,通过二分法快速定位到目标时间附近的两个数据点;
- 比较这两个点与目标时间的差值,取更接近的那个点的数值即可。
三、实现共享十字线与同步Tooltip
要让十字线在多个面板间同步,需要利用Grafana的事件总线(eventBus):
- 在当前面板触发光标移动事件时,通过
eventBus.publish()广播当前的时间戳; - 在其他自定义面板里,通过
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
相关产品推荐
相关产品推荐

