From 44e7be134dfa0ae6d31abbe771cab1d32b3df643 Mon Sep 17 00:00:00 2001 From: Leon Sorokin Date: Wed, 11 Jun 2025 15:43:54 -0500 Subject: [PATCH] TimeSeries: Fix rendering of sparse points with null gaps at outer edges (#106507) --- .../app/core/components/TimeSeries/utils.ts | 75 +++++++++++-------- 1 file changed, 43 insertions(+), 32 deletions(-) diff --git a/public/app/core/components/TimeSeries/utils.ts b/public/app/core/components/TimeSeries/utils.ts index 5328be7e863..b23a6a3b46d 100644 --- a/public/app/core/components/TimeSeries/utils.ts +++ b/public/app/core/components/TimeSeries/utils.ts @@ -347,48 +347,59 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn = ({ pointsFilter = (u, seriesIdx, show, gaps) => { let filtered = []; - let series = u.series[seriesIdx]; - - if (!show && gaps && gaps.length) { - const [firstIdx, lastIdx] = series.idxs!; - const xData = u.data[0]; + if (!show) { const yData = u.data[seriesIdx]; - const firstPos = Math.round(u.valToPos(xData[firstIdx], 'x', true)); - const lastPos = Math.round(u.valToPos(xData[lastIdx], 'x', true)); - if (gaps[0][0] === firstPos) { - filtered.push(firstIdx); - } + if (gaps && gaps.length) { + const firstIdx = u.posToIdx(gaps[0][0], true); - // show single points between consecutive gaps that share end/start - for (let i = 0; i < gaps.length; i++) { - let thisGap = gaps[i]; - let nextGap = gaps[i + 1]; + if (yData[firstIdx - 1] == null) { + filtered.push(firstIdx); + } - if (nextGap && thisGap[1] === nextGap[0]) { - // approx when data density is > 1pt/px, since gap start/end pixels are rounded - let approxIdx = u.posToIdx(thisGap[1], true); + // show single points between consecutive gaps that share end/start + for (let i = 0; i < gaps.length; i++) { + let thisGap = gaps[i]; + let nextGap = gaps[i + 1]; - if (yData[approxIdx] == null) { - // scan left/right alternating to find closest index with non-null value - for (let j = 1; j < 100; j++) { - if (yData[approxIdx + j] != null) { - approxIdx += j; - break; - } - if (yData[approxIdx - j] != null) { - approxIdx -= j; - break; + if (nextGap && thisGap[1] === nextGap[0]) { + // approx when data density is > 1pt/px, since gap start/end pixels are rounded + let approxIdx = u.posToIdx(thisGap[1], true); + + if (yData[approxIdx] == null) { + // scan left/right alternating to find closest index with non-null value + for (let j = 1; j < 100; j++) { + if (yData[approxIdx + j] != null) { + approxIdx += j; + break; + } + if (yData[approxIdx - j] != null) { + approxIdx -= j; + break; + } } } - } - filtered.push(approxIdx); + filtered.push(approxIdx); + } + } + + const lastIdx = u.posToIdx(gaps[gaps.length - 1][1], true); + + if (yData[lastIdx + 1] == null) { + filtered.push(lastIdx); } } - - if (gaps[gaps.length - 1][1] === lastPos) { - filtered.push(lastIdx); + // single point surrounded by nulls + else { + // meh heuristic + if (yData[0] == null && yData[yData.length - 1] == null) { + for (let i = 0; i < yData.length; i++) { + if (yData[i] != null) { + filtered.push(i); + } + } + } } }