From 066c9c8ff41e444496c9050d515fef4125284ec0 Mon Sep 17 00:00:00 2001 From: Leon Sorokin Date: Tue, 23 Mar 2021 01:00:34 -0500 Subject: [PATCH] GraphNG: accept number for spanNulls to indicate max threshold below which nulls are connected (#32146) --- .../panel-graph/graph-ng-nulls.json | 98 +++++++++++++++++++ .../transformers/joinDataFrames.ts | 3 +- .../GraphNG/nullToUndefThreshold.ts | 30 ++++++ .../src/components/GraphNG/utils.ts | 37 ++++++- .../grafana-ui/src/components/uPlot/config.ts | 8 +- .../uPlot/config/UPlotSeriesBuilder.ts | 2 +- 6 files changed, 172 insertions(+), 6 deletions(-) create mode 100644 packages/grafana-ui/src/components/GraphNG/nullToUndefThreshold.ts diff --git a/devenv/dev-dashboards/panel-graph/graph-ng-nulls.json b/devenv/dev-dashboards/panel-graph/graph-ng-nulls.json index 26766b74b05..0262ca92229 100644 --- a/devenv/dev-dashboards/panel-graph/graph-ng-nulls.json +++ b/devenv/dev-dashboards/panel-graph/graph-ng-nulls.json @@ -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, diff --git a/packages/grafana-data/src/transformations/transformers/joinDataFrames.ts b/packages/grafana-data/src/transformations/transformers/joinDataFrames.ts index f5966b7b8fb..872ced1715a 100644 --- a/packages/grafana-data/src/transformations/transformers/joinDataFrames.ts +++ b/packages/grafana-data/src/transformations/transformers/joinDataFrames.ts @@ -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, diff --git a/packages/grafana-ui/src/components/GraphNG/nullToUndefThreshold.ts b/packages/grafana-ui/src/components/GraphNG/nullToUndefThreshold.ts new file mode 100644 index 00000000000..dacc82a63fa --- /dev/null +++ b/packages/grafana-ui/src/components/GraphNG/nullToUndefThreshold.ts @@ -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; +} diff --git a/packages/grafana-ui/src/components/GraphNG/utils.ts b/packages/grafana-ui/src/components/GraphNG/utils.ts index e860003666d..3f60f87cfe7 100644 --- a/packages/grafana-ui/src/components/GraphNG/utils.ts +++ b/packages/grafana-ui/src/components/GraphNG/utils.ts @@ -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( diff --git a/packages/grafana-ui/src/components/uPlot/config.ts b/packages/grafana-ui/src/components/uPlot/config.ts index 43789d13bc5..86798a44072 100644 --- a/packages/grafana-ui/src/components/uPlot/config.ts +++ b/packages/grafana-ui/src/components/uPlot/config.ts @@ -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; } /** diff --git a/packages/grafana-ui/src/components/uPlot/config/UPlotSeriesBuilder.ts b/packages/grafana-ui/src/components/uPlot/config/UPlotSeriesBuilder.ts index ca6bc9da7f4..881c5d9bd1a 100755 --- a/packages/grafana-ui/src/components/uPlot/config/UPlotSeriesBuilder.ts +++ b/packages/grafana-ui/src/components/uPlot/config/UPlotSeriesBuilder.ts @@ -104,7 +104,7 @@ export class UPlotSeriesBuilder extends PlotConfigBuilder { return { scale: scaleKey, - spanGaps: spanNulls, + spanGaps: typeof spanNulls === 'number' ? false : spanNulls, pxAlign, show, fill: this.getFill(),