GraphNG: accept number for spanNulls to indicate max threshold below which nulls are connected (#32146)

This commit is contained in:
Leon Sorokin 2021-03-23 01:00:34 -05:00 committed by GitHub
parent da987caa60
commit 066c9c8ff4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 172 additions and 6 deletions

View File

@ -1026,6 +1026,104 @@
"title": "Null values in second series show gaps (bugged)",
"transformations": [],
"type": "timeseries"
},
{
"datasource": null,
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 10,
"gradientMode": "none",
"hideFrom": {
"graph": false,
"legend": false,
"tooltip": false
},
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "never",
"spanNulls": 3600000
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
},
"unit": "short"
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 10,
"x": 14,
"y": 14
},
"id": 13,
"options": {
"graph": {},
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom"
},
"tooltipOptions": {
"mode": "single"
}
},
"pluginVersion": "7.5.0-pre",
"targets": [
{
"alias": "",
"csvWave": {
"timeStep": 60,
"valuesCSV": "0,0,2,2,1,1"
},
"lines": 10,
"points": [],
"pulseWave": {
"offCount": 3,
"offValue": 1,
"onCount": 3,
"onValue": 2,
"timeStep": 60
},
"refId": "A",
"scenarioId": "csv_metric_values",
"stream": {
"bands": 1,
"noise": 2.2,
"speed": 250,
"spread": 3.5,
"type": "signal"
},
"stringInput": "1,20,90,null,30,5,0"
}
],
"timeFrom": null,
"timeShift": null,
"title": "Span nulls below 1hr",
"type": "timeseries"
}
],
"schemaVersion": 27,

View File

@ -134,7 +134,7 @@ export function outerJoinDataFrames(options: JoinOptions): DataFrame | undefined
}
// Support the standard graph span nulls field config
nullModesFrame.push(field.config.custom?.spanNulls ? NULL_REMOVE : NULL_EXPAND);
nullModesFrame.push(field.config.custom?.spanNulls === true ? NULL_REMOVE : NULL_EXPAND);
let labels = field.labels ?? {};
if (frame.name) {
@ -177,6 +177,7 @@ export function outerJoinDataFrames(options: JoinOptions): DataFrame | undefined
}
const joined = join(allData, nullModes);
return {
// ...options.data[0], // keep name, meta?
length: joined[0].length,

View File

@ -0,0 +1,30 @@
// mutates all nulls -> undefineds in the fieldValues array for value-less refValues ranges below maxThreshold
// refValues is typically a time array and maxThreshold is the allowable distance between in time
export function nullToUndefThreshold(refValues: number[], fieldValues: any[], maxThreshold: number): any[] {
let prevRef;
let nullIdx;
for (let i = 0; i < fieldValues.length; i++) {
let fieldVal = fieldValues[i];
if (fieldVal == null) {
if (nullIdx == null && prevRef != null) {
nullIdx = i;
}
} else {
if (nullIdx != null) {
if (refValues[i] - (prevRef as number) < maxThreshold) {
while (nullIdx < i) {
fieldValues[nullIdx++] = undefined;
}
}
nullIdx = null;
}
prevRef = refValues[i];
}
}
return fieldValues;
}

View File

@ -2,6 +2,7 @@ import React from 'react';
import isNumber from 'lodash/isNumber';
import { GraphNGLegendEventMode, XYFieldMatchers } from './types';
import {
ArrayVector,
DataFrame,
FieldConfig,
FieldType,
@ -14,6 +15,7 @@ import {
TimeRange,
TimeZone,
} from '@grafana/data';
import { nullToUndefThreshold } from './nullToUndefThreshold';
import { UPlotConfigBuilder } from '../uPlot/config/UPlotConfigBuilder';
import { FIXED_UNIT } from './GraphNG';
import {
@ -40,13 +42,42 @@ export function mapMouseEventToMode(event: React.MouseEvent): GraphNGLegendEvent
return GraphNGLegendEventMode.ToggleSelection;
}
export function preparePlotFrame(data: DataFrame[], dimFields: XYFieldMatchers) {
return outerJoinDataFrames({
frames: data,
function applySpanNullsThresholds(frames: DataFrame[]) {
for (const frame of frames) {
let refField = frame.fields.find((field) => field.type === FieldType.time); // this doesnt need to be time, just any numeric/asc join field
let refValues = refField?.values.toArray() as any[];
for (let i = 0; i < frame.fields.length; i++) {
let field = frame.fields[i];
if (field === refField) {
continue;
}
if (field.type === FieldType.number) {
let spanNulls = field.config.custom?.spanNulls;
if (typeof spanNulls === 'number') {
field.values = new ArrayVector(nullToUndefThreshold(refValues, field.values.toArray(), spanNulls));
}
}
}
}
return frames;
}
export function preparePlotFrame(frames: DataFrame[], dimFields: XYFieldMatchers) {
applySpanNullsThresholds(frames);
let joined = outerJoinDataFrames({
frames: frames,
joinBy: dimFields.x,
keep: dimFields.y,
keepOriginIndices: true,
});
return joined;
}
export function preparePlotConfigBuilder(

View File

@ -93,7 +93,13 @@ export interface LineConfig {
lineWidth?: number;
lineInterpolation?: LineInterpolation;
lineStyle?: LineStyle;
spanNulls?: boolean;
/**
* Indicate if null values should be treated as gaps or connected.
* When the value is a number, it represents the maximum delta in the
* X axis that should be considered connected. For timeseries, this is milliseconds
*/
spanNulls?: boolean | number;
}
/**

View File

@ -104,7 +104,7 @@ export class UPlotSeriesBuilder extends PlotConfigBuilder<SeriesProps, Series> {
return {
scale: scaleKey,
spanGaps: spanNulls,
spanGaps: typeof spanNulls === 'number' ? false : spanNulls,
pxAlign,
show,
fill: this.getFill(),