diff --git a/docs/sources/panels/visualizations/time-series/graph-time-series-as-lines.md b/docs/sources/panels/visualizations/time-series/graph-time-series-as-lines.md index 906dd703acb..ead096574dd 100644 --- a/docs/sources/panels/visualizations/time-series/graph-time-series-as-lines.md +++ b/docs/sources/panels/visualizations/time-series/graph-time-series-as-lines.md @@ -155,18 +155,28 @@ Dot spacing set to 0, 30: Choose how null values (gaps in the data) are displayed on the graph. -#### Gaps +#### Connect null values -If there is a gap in the series, the line in the graph will be broken and show the gap. +If there are null values in the series, the line can be connected or show gaps ![Null values gaps example](/img/docs/time-series-panel/line-graph-null-gaps-7-4.png) -#### Connected +#### Never -If there is a gap in the series, the line will skip the gap and connect to the next non-null value. +When values are missing in the data, they will be rendered as gaps in the line + +#### Always + +If there is a gap in the data, the line will be shown connected to the next non-null value ![Null values connected example](/img/docs/time-series-panel/line-graph-null-connected-7-4.png) +#### Threshold + +The threshold settings allows specifying a maximum time between points that should be connected. + + + ### Show points Choose when the points should be shown on the graph. diff --git a/public/app/plugins/panel/timeseries/SpanNullsEditor.tsx b/public/app/plugins/panel/timeseries/SpanNullsEditor.tsx new file mode 100644 index 00000000000..c7cef3258a3 --- /dev/null +++ b/public/app/plugins/panel/timeseries/SpanNullsEditor.tsx @@ -0,0 +1,65 @@ +import React from 'react'; +import { FieldOverrideEditorProps, rangeUtil, SelectableValue } from '@grafana/data'; +import { HorizontalGroup, Input, RadioButtonGroup } from '@grafana/ui'; + +const GAPS_OPTIONS: Array> = [ + { + label: 'Never', + value: false, + }, + { + label: 'Always', + value: true, + }, + { + label: 'Threshold', + value: 3600000, // 1h + }, +]; + +export const SpanNullsEditor: React.FC> = ({ value, onChange }) => { + const isThreshold = typeof value === 'number'; + const formattedTime = isThreshold ? rangeUtil.secondsToHms((value as number) / 1000) : undefined; + GAPS_OPTIONS[2].value = isThreshold ? (value as number) : 3600000; // 1h + + const checkAndUpdate = (txt: string) => { + let val: boolean | number = false; + if (txt) { + try { + val = rangeUtil.intervalToSeconds(txt) * 1000; + } catch (err) { + console.warn('ERROR', err); + } + } + onChange(val); + }; + + const handleEnterKey = (e: React.KeyboardEvent) => { + if (e.key !== 'Enter') { + return; + } + checkAndUpdate((e.target as any).value); + }; + + const handleBlur = (e: React.FocusEvent) => { + checkAndUpdate(e.target.value); + }; + + return ( + + + {isThreshold && ( + <} + spellCheck={false} + /> + )} + + ); +}; diff --git a/public/app/plugins/panel/timeseries/config.ts b/public/app/plugins/panel/timeseries/config.ts index 7bc9e3abd3a..1a2613a529b 100644 --- a/public/app/plugins/panel/timeseries/config.ts +++ b/public/app/plugins/panel/timeseries/config.ts @@ -30,6 +30,7 @@ import { ScaleDistributionEditor } from './ScaleDistributionEditor'; import { LineStyleEditor } from './LineStyleEditor'; import { FillBellowToEditor } from './FillBelowToEditor'; import { OptionsWithLegend } from './types'; +import { SpanNullsEditor } from './SpanNullsEditor'; export const defaultGraphConfig: GraphFieldConfig = { drawStyle: DrawStyle.Line, @@ -133,17 +134,16 @@ export function getGraphFieldConfig(cfg: GraphFieldConfig): SetFieldConfigOption process: identityOverrideProcessor, shouldApply: (f) => f.type === FieldType.number, }) - .addRadio({ + .addCustomEditor({ + id: 'spanNulls', path: 'spanNulls', - name: 'Null values', + name: 'Connect null values', defaultValue: false, - settings: { - options: [ - { label: 'Gaps', value: false }, - { label: 'Connected', value: true }, - ], - }, + editor: SpanNullsEditor, + override: SpanNullsEditor, showIf: (c) => c.drawStyle === DrawStyle.Line, + shouldApply: (f) => f.type !== FieldType.time, + process: identityOverrideProcessor, }) .addRadio({ path: 'showPoints',