TimeSeries: Render null-bounded points at data edges (#57798)

This commit is contained in:
Leon Sorokin 2022-10-31 17:50:43 -05:00 committed by GitHub
parent fbfd0fd51b
commit 45234e76eb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 55 additions and 1 deletions

View File

@ -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",

View File

@ -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,

View File

@ -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",

View File

@ -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<SeriesProps, Series> {
@ -71,6 +73,7 @@ export class UPlotSeriesBuilder extends PlotConfigBuilder<SeriesProps, Series> {
pxAlign,
spanNulls,
show = true,
gapsRefiner,
} = this.props;
let lineConfig: Partial<Series> = {};
@ -145,6 +148,7 @@ export class UPlotSeriesBuilder extends PlotConfigBuilder<SeriesProps, Series> {
pxAlign,
show,
fill: this.getFill(),
gaps: gapsRefiner ?? ((u, seriesIdx, idx0, idx1, gaps) => gaps),
...lineConfig,
...pointsConfig,
};

View File

@ -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",