diff --git a/packages/grafana-ui/src/components/TimeSeries/utils.ts b/packages/grafana-ui/src/components/TimeSeries/utils.ts index 500c357a5ad..994fd0bc5db 100644 --- a/packages/grafana-ui/src/components/TimeSeries/utils.ts +++ b/packages/grafana-ui/src/components/TimeSeries/utils.ts @@ -344,19 +344,19 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn<{ sync: DashboardCursor let seriesData = self.data[seriesIdx]; if (seriesData[hoveredIdx] == null) { - let nonNullLft = hoveredIdx, - nonNullRgt = hoveredIdx, + let nonNullLft = null, + nonNullRgt = null, i; i = hoveredIdx; - while (nonNullLft === hoveredIdx && i-- > 0) { + while (nonNullLft == null && i-- > 0) { if (seriesData[i] != null) { nonNullLft = i; } } i = hoveredIdx; - while (nonNullRgt === hoveredIdx && i++ < seriesData.length) { + while (nonNullRgt == null && i++ < seriesData.length) { if (seriesData[i] != null) { nonNullRgt = i; } @@ -365,19 +365,19 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn<{ sync: DashboardCursor let xVals = self.data[0]; let curPos = self.valToPos(cursorXVal, 'x'); - let rgtPos = self.valToPos(xVals[nonNullRgt], 'x'); - let lftPos = self.valToPos(xVals[nonNullLft], 'x'); + let rgtPos = nonNullRgt == null ? Infinity : self.valToPos(xVals[nonNullRgt], 'x'); + let lftPos = nonNullLft == null ? -Infinity : self.valToPos(xVals[nonNullLft], 'x'); let lftDelta = curPos - lftPos; let rgtDelta = rgtPos - curPos; if (lftDelta <= rgtDelta) { if (lftDelta <= hoverProximityPx) { - hoveredIdx = nonNullLft; + hoveredIdx = nonNullLft!; } } else { if (rgtDelta <= hoverProximityPx) { - hoveredIdx = nonNullRgt; + hoveredIdx = nonNullRgt!; } } } diff --git a/packages/grafana-ui/src/components/uPlot/plugins/TooltipPlugin.tsx b/packages/grafana-ui/src/components/uPlot/plugins/TooltipPlugin.tsx index 923708e218d..4a8fb96eba3 100644 --- a/packages/grafana-ui/src/components/uPlot/plugins/TooltipPlugin.tsx +++ b/packages/grafana-ui/src/components/uPlot/plugins/TooltipPlugin.tsx @@ -1,3 +1,6 @@ +import React, { useEffect, useLayoutEffect, useRef, useState } from 'react'; +import { useMountedState } from 'react-use'; +import uPlot from 'uplot'; import { CartesianCoords2D, DashboardCursorSync, @@ -10,9 +13,6 @@ import { TimeZone, } from '@grafana/data'; import { TooltipDisplayMode } from '@grafana/schema'; -import React, { useEffect, useLayoutEffect, useState } from 'react'; -import { useMountedState } from 'react-use'; -import uPlot from 'uplot'; import { useTheme2 } from '../../../themes/ThemeContext'; import { Portal } from '../../Portal/Portal'; import { SeriesTable, SeriesTableRowProps, VizTooltipContainer } from '../../VizTooltip'; @@ -43,6 +43,7 @@ export const TooltipPlugin: React.FC = ({ renderTooltip, ...otherProps }) => { + const plotInstance = useRef(); const theme = useTheme2(); const [focusedSeriesIdx, setFocusedSeriesIdx] = useState(null); const [focusedPointIdx, setFocusedPointIdx] = useState(null); @@ -60,7 +61,6 @@ export const TooltipPlugin: React.FC = ({ // Add uPlot hooks to the config, or re-add when the config changed useLayoutEffect(() => { - let plotInstance: uPlot | undefined = undefined; let bbox: DOMRect | undefined = undefined; const plotMouseLeave = () => { @@ -69,7 +69,7 @@ export const TooltipPlugin: React.FC = ({ } setCoords(null); setIsActive(false); - plotInstance?.root.classList.remove('plot-active'); + plotInstance.current?.root.classList.remove('plot-active'); }; const plotMouseEnter = () => { @@ -77,16 +77,14 @@ export const TooltipPlugin: React.FC = ({ return; } setIsActive(true); - plotInstance?.root.classList.add('plot-active'); + plotInstance.current?.root.classList.add('plot-active'); }; // cache uPlot plotting area bounding box - config.addHook('syncRect', (u, rect) => { - bbox = rect; - }); + config.addHook('syncRect', (u, rect) => (bbox = rect)); config.addHook('init', (u) => { - plotInstance = u; + plotInstance.current = u; u.over.addEventListener('mouseleave', plotMouseLeave); u.over.addEventListener('mouseenter', plotMouseEnter); @@ -155,9 +153,9 @@ export const TooltipPlugin: React.FC = ({ return () => { setCoords(null); - if (plotInstance) { - plotInstance.over.removeEventListener('mouseleave', plotMouseLeave); - plotInstance.over.removeEventListener('mouseenter', plotMouseEnter); + if (plotInstance.current) { + plotInstance.current.over.removeEventListener('mouseleave', plotMouseLeave); + plotInstance.current.over.removeEventListener('mouseenter', plotMouseEnter); } }; }, [config, setCoords, setIsActive, setFocusedPointIdx, setFocusedPointIdxs]); @@ -174,7 +172,7 @@ export const TooltipPlugin: React.FC = ({ const xFieldFmt = xField.display || getDisplayProcessor({ field: xField, timeZone, theme }); let tooltip: React.ReactNode = null; - const xVal = xFieldFmt(xField!.values.get(focusedPointIdx)).text; + let xVal = xFieldFmt(xField!.values.get(focusedPointIdx)).text; if (!renderTooltip) { // when interacting with a point in single mode @@ -185,8 +183,10 @@ export const TooltipPlugin: React.FC = ({ return null; } + const dataIdx = focusedPointIdxs?.[focusedSeriesIdx] ?? focusedPointIdx; + xVal = xFieldFmt(xField!.values.get(dataIdx)).text; const fieldFmt = field.display || getDisplayProcessor({ field, timeZone, theme }); - const display = fieldFmt(field.values.get(focusedPointIdx)); + const display = fieldFmt(field.values.get(dataIdx)); tooltip = ( = ({ ); }; -function isCursourOutsideCanvas({ left, top }: uPlot.Cursor, canvas: DOMRect) { +function isCursorOutsideCanvas({ left, top }: uPlot.Cursor, canvas: DOMRect) { if (left === undefined || top === undefined) { return false; } @@ -264,7 +264,7 @@ export function positionTooltip(u: uPlot, bbox: DOMRect) { const cL = u.cursor.left || 0; const cT = u.cursor.top || 0; - if (isCursourOutsideCanvas(u.cursor, bbox)) { + if (isCursorOutsideCanvas(u.cursor, bbox)) { const idx = u.posToIdx(cL); // when cursor outside of uPlot's canvas if (cT < 0 || cT > bbox.height) {