From 45234e76eb82a417d8cee236d7a2ad91dc580bbb Mon Sep 17 00:00:00 2001 From: Leon Sorokin Date: Mon, 31 Oct 2022 17:50:43 -0500 Subject: [PATCH] TimeSeries: Render null-bounded points at data edges (#57798) --- .../GraphNG/__snapshots__/utils.test.ts.snap | 5 +++ .../src/components/TimeSeries/utils.ts | 35 ++++++++++++++++++- .../uPlot/config/UPlotConfigBuilder.test.ts | 4 +++ .../uPlot/config/UPlotSeriesBuilder.ts | 4 +++ .../barchart/__snapshots__/utils.test.ts.snap | 8 +++++ 5 files changed, 55 insertions(+), 1 deletion(-) 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 05d9b60120e..f9c3fbbab73 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 @@ -129,6 +129,7 @@ Object { ], "facets": undefined, "fill": [Function], + "gaps": [Function], "paths": [Function], "points": Object { "fill": "#ff0000", @@ -152,6 +153,7 @@ Object { ], "facets": undefined, "fill": [Function], + "gaps": [Function], "paths": [Function], "points": Object { "fill": "#ff0000", @@ -175,6 +177,7 @@ Object { ], "facets": undefined, "fill": [Function], + "gaps": [Function], "paths": [Function], "points": Object { "fill": "#ff0000", @@ -198,6 +201,7 @@ Object { ], "facets": undefined, "fill": [Function], + "gaps": [Function], "paths": [Function], "points": Object { "fill": "#ff0000", @@ -221,6 +225,7 @@ Object { ], "facets": undefined, "fill": [Function], + "gaps": [Function], "paths": [Function], "points": Object { "fill": "#ff0000", diff --git a/packages/grafana-ui/src/components/TimeSeries/utils.ts b/packages/grafana-ui/src/components/TimeSeries/utils.ts index 26cf4f1477d..26fafa14197 100644 --- a/packages/grafana-ui/src/components/TimeSeries/utils.ts +++ b/packages/grafana-ui/src/components/TimeSeries/utils.ts @@ -296,7 +296,7 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn<{ let pointsFilter: uPlot.Series.Points.Filter = () => null; - if (customConfig.spanNulls !== true) { + if (customConfig.spanNulls !== true && showPoints !== VisibilityMode.Always) { pointsFilter = (u, seriesIdx, show, gaps) => { let filtered = []; @@ -426,7 +426,40 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn<{ dynamicSeriesColor = (seriesIdx) => getFieldSeriesColor(alignedFrame.fields[seriesIdx], theme).color; } + // this adds leading and trailing gaps when datasets have leading and trailing nulls + // it will cause additional unnecessary clips, but we also use adjacent gaps to show single points + // when not connecting across gaps, e.g. null,100,null,null,50,50,50,null,50,null,null + const gapsRefiner: uPlot.Series.GapsRefiner = (u, seriesIdx, idx0, idx1, gaps) => { + let yData = u.data[seriesIdx]; + + // @ts-ignore + let xData = u._data[0]; + + // scan to first and last non-null vals + let first = idx0, + last = idx1; + + while (first <= last && yData[first] == null) { + first++; + } + + while (last > first && yData[last] == null) { + last--; + } + + if (first !== idx0) { + gaps.unshift([u.bbox.left, Math.round(u.valToPos(xData[first]!, 'x', true))]); + } + + if (last !== idx1) { + gaps.push([Math.round(u.valToPos(xData[last]!, 'x', true)), u.bbox.left + u.bbox.width]); + } + + return gaps; + }; + builder.addSeries({ + gapsRefiner, pathBuilder, pointsBuilder, scaleKey, diff --git a/packages/grafana-ui/src/components/uPlot/config/UPlotConfigBuilder.test.ts b/packages/grafana-ui/src/components/uPlot/config/UPlotConfigBuilder.test.ts index a32fadd34d7..a4714bddd7b 100644 --- a/packages/grafana-ui/src/components/uPlot/config/UPlotConfigBuilder.test.ts +++ b/packages/grafana-ui/src/components/uPlot/config/UPlotConfigBuilder.test.ts @@ -613,6 +613,7 @@ describe('UPlotConfigBuilder', () => { Object { "facets": undefined, "fill": [Function], + "gaps": [Function], "paths": [Function], "points": Object { "fill": "#0000ff", @@ -740,6 +741,7 @@ describe('UPlotConfigBuilder', () => { Object { "facets": undefined, "fill": [Function], + "gaps": [Function], "paths": [Function], "points": Object { "fill": "#0000ff", @@ -758,6 +760,7 @@ describe('UPlotConfigBuilder', () => { Object { "facets": undefined, "fill": [Function], + "gaps": [Function], "paths": [Function], "points": Object { "fill": "#00ff00", @@ -776,6 +779,7 @@ describe('UPlotConfigBuilder', () => { Object { "facets": undefined, "fill": [Function], + "gaps": [Function], "paths": [Function], "points": Object { "fill": "#ff0000", diff --git a/packages/grafana-ui/src/components/uPlot/config/UPlotSeriesBuilder.ts b/packages/grafana-ui/src/components/uPlot/config/UPlotSeriesBuilder.ts index cddb4918c57..441a094c81b 100644 --- a/packages/grafana-ui/src/components/uPlot/config/UPlotSeriesBuilder.ts +++ b/packages/grafana-ui/src/components/uPlot/config/UPlotSeriesBuilder.ts @@ -49,6 +49,8 @@ export interface SeriesProps extends LineConfig, BarConfig, FillConfig, PointsCo dataFrameFieldIndex?: DataFrameFieldIndex; theme: GrafanaTheme2; value?: uPlot.Series.Value; + + gapsRefiner?: uPlot.Series.GapsRefiner; } export class UPlotSeriesBuilder extends PlotConfigBuilder { @@ -71,6 +73,7 @@ export class UPlotSeriesBuilder extends PlotConfigBuilder { pxAlign, spanNulls, show = true, + gapsRefiner, } = this.props; let lineConfig: Partial = {}; @@ -145,6 +148,7 @@ export class UPlotSeriesBuilder extends PlotConfigBuilder { pxAlign, show, fill: this.getFill(), + gaps: gapsRefiner ?? ((u, seriesIdx, idx0, idx1, gaps) => gaps), ...lineConfig, ...pointsConfig, }; diff --git a/public/app/plugins/panel/barchart/__snapshots__/utils.test.ts.snap b/public/app/plugins/panel/barchart/__snapshots__/utils.test.ts.snap index eb7fa961463..874d31dbfd9 100644 --- a/public/app/plugins/panel/barchart/__snapshots__/utils.test.ts.snap +++ b/public/app/plugins/panel/barchart/__snapshots__/utils.test.ts.snap @@ -133,6 +133,7 @@ Object { Object { "facets": undefined, "fill": [Function], + "gaps": [Function], "paths": [Function], "points": Object { "fill": "#808080", @@ -286,6 +287,7 @@ Object { Object { "facets": undefined, "fill": [Function], + "gaps": [Function], "paths": [Function], "points": Object { "fill": "#808080", @@ -439,6 +441,7 @@ Object { Object { "facets": undefined, "fill": [Function], + "gaps": [Function], "paths": [Function], "points": Object { "fill": "#808080", @@ -592,6 +595,7 @@ Object { Object { "facets": undefined, "fill": [Function], + "gaps": [Function], "paths": [Function], "points": Object { "fill": "#808080", @@ -745,6 +749,7 @@ Object { Object { "facets": undefined, "fill": [Function], + "gaps": [Function], "paths": [Function], "points": Object { "fill": "#808080", @@ -898,6 +903,7 @@ Object { Object { "facets": undefined, "fill": [Function], + "gaps": [Function], "paths": [Function], "points": Object { "fill": "#808080", @@ -1051,6 +1057,7 @@ Object { Object { "facets": undefined, "fill": [Function], + "gaps": [Function], "paths": [Function], "points": Object { "fill": "#808080", @@ -1204,6 +1211,7 @@ Object { Object { "facets": undefined, "fill": [Function], + "gaps": [Function], "paths": [Function], "points": Object { "fill": "#808080",