From 0a08cf10e52bc2a1b76e9acde88393fc7d79bb2e Mon Sep 17 00:00:00 2001 From: "Grot (@grafanabot)" <43478413+grafanabot@users.noreply.github.com> Date: Sat, 24 Jul 2021 00:23:15 +0200 Subject: [PATCH] TimeSeries: make cursor hover the nearest non-null/undefined datapoint (#34552) (#37171) (cherry picked from commit 4c3e197e262f02a2417b62ed158f454d11afcdf1) Co-authored-by: Leon Sorokin --- .../GraphNG/__snapshots__/utils.test.ts.snap | 1 + .../src/components/GraphNG/utils.ts | 4 +- .../src/components/TimeSeries/utils.ts | 100 +++++++++++++----- .../uPlot/plugins/TooltipPlugin.tsx | 10 +- 4 files changed, 87 insertions(+), 28 deletions(-) diff --git a/packages/grafana-ui/src/components/GraphNG/__snapshots__/utils.test.ts.snap b/packages/grafana-ui/src/components/GraphNG/__snapshots__/utils.test.ts.snap index 10283981b35..cb007527ece 100644 --- a/packages/grafana-ui/src/components/GraphNG/__snapshots__/utils.test.ts.snap +++ b/packages/grafana-ui/src/components/GraphNG/__snapshots__/utils.test.ts.snap @@ -69,6 +69,7 @@ Object { }, ], "cursor": Object { + "dataIdx": [Function], "drag": Object { "setScale": false, }, diff --git a/packages/grafana-ui/src/components/GraphNG/utils.ts b/packages/grafana-ui/src/components/GraphNG/utils.ts index 8d3b0c44751..dd07370a82c 100644 --- a/packages/grafana-ui/src/components/GraphNG/utils.ts +++ b/packages/grafana-ui/src/components/GraphNG/utils.ts @@ -18,7 +18,9 @@ function applySpanNullsThresholds(frame: DataFrame) { let spanNulls = field.config.custom?.spanNulls; if (typeof spanNulls === 'number') { - field.values = new ArrayVector(nullToUndefThreshold(refValues, field.values.toArray(), spanNulls)); + if (spanNulls !== -1) { + field.values = new ArrayVector(nullToUndefThreshold(refValues, field.values.toArray(), spanNulls)); + } } } } diff --git a/packages/grafana-ui/src/components/TimeSeries/utils.ts b/packages/grafana-ui/src/components/TimeSeries/utils.ts index 8d5de8a672c..f6c77877d3d 100644 --- a/packages/grafana-ui/src/components/TimeSeries/utils.ts +++ b/packages/grafana-ui/src/components/TimeSeries/utils.ts @@ -262,6 +262,58 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn<{ sync: DashboardCursor builder.scaleKeys = [xScaleKey, yScaleKey]; + // if hovered value is null, how far we may scan left/right to hover nearest non-null + const hoverProximityPx = 15; + + let cursor: Partial = { + // this scans left and right from cursor position to find nearest data index with value != null + // TODO: do we want to only scan past undefined values, but halt at explicit null values? + dataIdx: (self, seriesIdx, hoveredIdx, cursorXVal) => { + let seriesData = self.data[seriesIdx]; + + if (seriesData[hoveredIdx] == null) { + let nonNullLft = hoveredIdx, + nonNullRgt = hoveredIdx, + i; + + i = hoveredIdx; + while (nonNullLft === hoveredIdx && i-- > 0) { + if (seriesData[i] != null) { + nonNullLft = i; + } + } + + i = hoveredIdx; + while (nonNullRgt === hoveredIdx && i++ < seriesData.length) { + if (seriesData[i] != null) { + nonNullRgt = i; + } + } + + 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 lftDelta = curPos - lftPos; + let rgtDelta = rgtPos - curPos; + + if (lftDelta <= rgtDelta) { + if (lftDelta <= hoverProximityPx) { + hoveredIdx = nonNullLft; + } + } else { + if (rgtDelta <= hoverProximityPx) { + hoveredIdx = nonNullRgt; + } + } + } + + return hoveredIdx; + }, + }; + if (sync !== DashboardCursorSync.Off) { const payload: DataHoverPayload = { point: { @@ -271,34 +323,34 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn<{ sync: DashboardCursor data: frame, }; const hoverEvent = new DataHoverEvent(payload); - builder.setSync(); - builder.setCursor({ - sync: { - key: '__global_', - filters: { - pub: (type: string, src: uPlot, x: number, y: number, w: number, h: number, dataIdx: number) => { - payload.columnIndex = dataIdx; - if (x < 0 && y < 0) { - payload.point[xScaleUnit] = null; - payload.point[yScaleKey] = null; - eventBus.publish(new DataHoverClearEvent(payload)); - } else { - // convert the points - payload.point[xScaleUnit] = src.posToVal(x, xScaleKey); - payload.point[yScaleKey] = src.posToVal(y, yScaleKey); - eventBus.publish(hoverEvent); - hoverEvent.payload.down = undefined; - } - return true; - }, + cursor.sync = { + key: '__global_', + filters: { + pub: (type: string, src: uPlot, x: number, y: number, w: number, h: number, dataIdx: number) => { + payload.columnIndex = dataIdx; + if (x < 0 && y < 0) { + payload.point[xScaleUnit] = null; + payload.point[yScaleKey] = null; + eventBus.publish(new DataHoverClearEvent(payload)); + } else { + // convert the points + payload.point[xScaleUnit] = src.posToVal(x, xScaleKey); + payload.point[yScaleKey] = src.posToVal(y, yScaleKey); + eventBus.publish(hoverEvent); + hoverEvent.payload.down = undefined; + } + return true; }, - // ??? setSeries: syncMode === DashboardCursorSync.Tooltip, - scales: builder.scaleKeys, - match: [() => true, () => true], }, - }); + // ??? setSeries: syncMode === DashboardCursorSync.Tooltip, + scales: builder.scaleKeys, + match: [() => true, () => true], + }; } + builder.setSync(); + builder.setCursor(cursor); + return builder; }; diff --git a/packages/grafana-ui/src/components/uPlot/plugins/TooltipPlugin.tsx b/packages/grafana-ui/src/components/uPlot/plugins/TooltipPlugin.tsx index 2894002831b..b063df8cdc1 100644 --- a/packages/grafana-ui/src/components/uPlot/plugins/TooltipPlugin.tsx +++ b/packages/grafana-ui/src/components/uPlot/plugins/TooltipPlugin.tsx @@ -43,6 +43,7 @@ export const TooltipPlugin: React.FC = ({ const plotCtx = usePlotContext(); const [focusedSeriesIdx, setFocusedSeriesIdx] = useState(null); const [focusedPointIdx, setFocusedPointIdx] = useState(null); + const [focusedPointIdxs, setFocusedPointIdxs] = useState>([]); const [coords, setCoords] = useState(null); const plotInstance = plotCtx.plot; @@ -93,10 +94,13 @@ export const TooltipPlugin: React.FC = ({ })(u); }); } else { + config.addHook('setLegend', (u) => { + setFocusedPointIdx(u.cursor.idx!); + setFocusedPointIdxs(u.cursor.idxs!.slice()); + }); + // default series/datapoint idx retireval config.addHook('setCursor', (u) => { - setFocusedPointIdx(u.cursor.idx === undefined ? u.posToIdx(u.cursor.left || 0) : u.cursor.idx); - const bbox = plotCtx.getCanvasBoundingBox(); if (!bbox) { return; @@ -174,7 +178,7 @@ export const TooltipPlugin: React.FC = ({ continue; } - const display = field.display!(otherProps.data.fields[i].values.get(focusedPointIdx)); + const display = field.display!(otherProps.data.fields[i].values.get(focusedPointIdxs[i]!)); series.push({ color: display.color || FALLBACK_COLOR,