mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
GraphNG: accept number for spanNulls to indicate max threshold below which nulls are connected (#32146)
This commit is contained in:
parent
da987caa60
commit
066c9c8ff4
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
}
|
@ -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(
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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(),
|
||||
|
Loading…
Reference in New Issue
Block a user