diff --git a/public/app/plugins/panel/geomap/layers/data/MarkersLegend.tsx b/public/app/plugins/panel/geomap/layers/data/MarkersLegend.tsx index b2fec9723b6..7197525cd1c 100644 --- a/public/app/plugins/panel/geomap/layers/data/MarkersLegend.tsx +++ b/public/app/plugins/panel/geomap/layers/data/MarkersLegend.tsx @@ -1,9 +1,10 @@ import React from 'react'; -import { Label, stylesFactory, useTheme2 } from '@grafana/ui'; +import { Label, stylesFactory, useTheme2, VizLegendItem } from '@grafana/ui'; import { formattedValueToString, getFieldColorModeForField, GrafanaTheme2 } from '@grafana/data'; import { css } from '@emotion/css'; import { config } from 'app/core/config'; import { DimensionSupplier } from 'app/features/dimensions'; +import { getThresholdItems } from 'app/plugins/panel/state-timeline/utils'; import { getMinMaxAndDelta } from '../../../../../../../packages/grafana-data/src/field/scale'; export interface MarkersLegendProps { @@ -56,33 +57,17 @@ export function MarkersLegend(props: MarkersLegendProps) { return
; // don't show anything in the legend } + const items = getThresholdItems(color.field!.config, config.theme2); return (
- {thresholds && ( -
- {thresholds.steps.map((step: any, idx: number) => { - const next = thresholds!.steps[idx + 1]; - let info = ?; - if (idx === 0) { - info = < {fmt(next.value)}; - } else if (next) { - info = ( - - {fmt(step.value)} - {fmt(next.value)} - - ); - } else { - info = {fmt(step.value)} +; - } - return ( -
- - {info} -
- ); - })} -
- )} +
+ {items.map((item: VizLegendItem, idx: number) => ( +
+ + {item.label} +
+ ))} +
); } diff --git a/public/app/plugins/panel/state-timeline/StateTimelinePanel.tsx b/public/app/plugins/panel/state-timeline/StateTimelinePanel.tsx index 06671805735..096c8e27798 100755 --- a/public/app/plugins/panel/state-timeline/StateTimelinePanel.tsx +++ b/public/app/plugins/panel/state-timeline/StateTimelinePanel.tsx @@ -23,9 +23,10 @@ export const StateTimelinePanel: React.FC = ({ }) => { const theme = useTheme2(); - const { frames, warn } = useMemo(() => prepareTimelineFields(data?.series, options.mergeValues ?? true), [ + const { frames, warn } = useMemo(() => prepareTimelineFields(data?.series, options.mergeValues ?? true, theme), [ data, options.mergeValues, + theme, ]); const legendItems = useMemo(() => prepareTimelineLegendItems(frames, options.legend, theme), [ diff --git a/public/app/plugins/panel/state-timeline/utils.test.ts b/public/app/plugins/panel/state-timeline/utils.test.ts index 0124bd1e60a..b45ca35e38e 100644 --- a/public/app/plugins/panel/state-timeline/utils.test.ts +++ b/public/app/plugins/panel/state-timeline/utils.test.ts @@ -1,6 +1,8 @@ -import { ArrayVector, FieldType, toDataFrame } from '@grafana/data'; +import { ArrayVector, createTheme, FieldType, toDataFrame } from '@grafana/data'; import { findNextStateIndex, prepareTimelineFields } from './utils'; +const theme = createTheme(); + describe('prepare timeline graph', () => { it('errors with no time fields', () => { const frames = [ @@ -11,7 +13,7 @@ describe('prepare timeline graph', () => { ], }), ]; - const info = prepareTimelineFields(frames, true); + const info = prepareTimelineFields(frames, true, theme); expect(info.warn).toEqual('Data does not have a time field'); }); @@ -24,7 +26,7 @@ describe('prepare timeline graph', () => { ], }), ]; - const info = prepareTimelineFields(frames, true); + const info = prepareTimelineFields(frames, true, theme); expect(info.warn).toEqual('No graphable fields'); }); @@ -37,7 +39,7 @@ describe('prepare timeline graph', () => { ], }), ]; - const info = prepareTimelineFields(frames, true); + const info = prepareTimelineFields(frames, true, theme); expect(info.warn).toBeUndefined(); const out = info.frames![0]; diff --git a/public/app/plugins/panel/state-timeline/utils.ts b/public/app/plugins/panel/state-timeline/utils.ts index 2cac419a049..7397aa2e038 100644 --- a/public/app/plugins/panel/state-timeline/utils.ts +++ b/public/app/plugins/panel/state-timeline/utils.ts @@ -12,6 +12,9 @@ import { getFieldDisplayName, getValueFormat, GrafanaTheme2, + getActiveThreshold, + Threshold, + getFieldConfigWithMinMax, outerJoinDataFrames, ThresholdsMode, } from '@grafana/data'; @@ -257,10 +260,73 @@ export function unsetSameFutureValues(values: any[]): any[] | undefined { return clone; } +/** + * Merge values by the threshold + */ +export function mergeThresholdValues(field: Field, theme: GrafanaTheme2): Field | undefined { + const thresholds = field.config.thresholds; + if (field.type !== FieldType.number || !thresholds || !thresholds.steps.length) { + return undefined; + } + + const items = getThresholdItems(field.config, theme); + if (items.length !== thresholds.steps.length) { + return undefined; // should not happen + } + + const thresholdToText = new Map(); + const textToColor = new Map(); + for (let i = 0; i < items.length; i++) { + thresholdToText.set(thresholds.steps[i], items[i].label); + textToColor.set(items[i].label, items[i].color!); + } + + let prev: Threshold | undefined = undefined; + let input = field.values.toArray(); + const vals = new Array(field.values.length); + if (thresholds.mode === ThresholdsMode.Percentage) { + const { min, max } = getFieldConfigWithMinMax(field); + const delta = max! - min!; + input = input.map((v) => { + if (v == null) { + return v; + } + return ((v - min!) / delta) * 100; + }); + } + + for (let i = 0; i < vals.length; i++) { + const v = input[i]; + if (v == null) { + vals[i] = v; + prev = undefined; + } + const active = getActiveThreshold(v, thresholds.steps); + if (active === prev) { + vals[i] = undefined; + } else { + vals[i] = thresholdToText.get(active); + } + prev = active; + } + + return { + ...field, + type: FieldType.string, + values: new ArrayVector(vals), + display: (value: string) => ({ + text: value, + color: textToColor.get(value), + numeric: NaN, + }), + }; +} + // This will return a set of frames with only graphable values included export function prepareTimelineFields( series: DataFrame[] | undefined, - mergeValues: boolean + mergeValues: boolean, + theme: GrafanaTheme2 ): { frames?: DataFrame[]; warn?: string } { if (!series?.length) { return { warn: 'No data in response' }; @@ -279,6 +345,15 @@ export function prepareTimelineFields( fields.push(field); break; case FieldType.number: + if (mergeValues && field.config.color?.mode === FieldColorModeId.Thresholds) { + const f = mergeThresholdValues(field, theme); + if (f) { + fields.push(f); + changed = true; + continue; + } + } + case FieldType.boolean: case FieldType.string: field = { @@ -332,6 +407,30 @@ export function prepareTimelineFields( return { frames }; } +export function getThresholdItems(fieldConfig: FieldConfig, theme: GrafanaTheme2): VizLegendItem[] { + const items: VizLegendItem[] = []; + const thresholds = fieldConfig.thresholds; + if (!thresholds || !thresholds.steps.length) { + return items; + } + + const steps = thresholds.steps; + const disp = getValueFormat(thresholds.mode === ThresholdsMode.Percentage ? 'percent' : fieldConfig.unit ?? ''); + + const fmt = (v: number) => formattedValueToString(disp(v)); + + for (let i = 1; i <= steps.length; i++) { + const step = steps[i - 1]; + items.push({ + label: i === 1 ? `< ${fmt(steps[i].value)}` : `${fmt(step.value)}+`, + color: theme.visualization.getColorByName(step.color), + yAxis: 1, + }); + } + + return items; +} + export function prepareTimelineLegendItems( frames: DataFrame[] | undefined, options: VizLegendOptions, @@ -353,21 +452,7 @@ export function prepareTimelineLegendItems( // If thresholds are enabled show each step in the legend if (colorMode === FieldColorModeId.Thresholds && thresholds?.steps && thresholds.steps.length > 1) { - const steps = thresholds.steps; - const disp = getValueFormat(thresholds.mode === ThresholdsMode.Percentage ? 'percent' : fieldConfig.unit ?? ''); - - const fmt = (v: number) => formattedValueToString(disp(v)); - - for (let i = 1; i <= steps.length; i++) { - const step = steps[i - 1]; - items.push({ - label: i === 1 ? `< ${fmt(steps[i].value)}` : `${fmt(step.value)}+`, - color: theme.visualization.getColorByName(step.color), - yAxis: 1, - }); - } - - return items; + return getThresholdItems(fieldConfig, theme); } // If thresholds are enabled show each step in the legend diff --git a/public/app/plugins/panel/status-history/StatusHistoryPanel.tsx b/public/app/plugins/panel/status-history/StatusHistoryPanel.tsx index 8d2906ff0d4..c9f5866a8b1 100755 --- a/public/app/plugins/panel/status-history/StatusHistoryPanel.tsx +++ b/public/app/plugins/panel/status-history/StatusHistoryPanel.tsx @@ -22,7 +22,7 @@ export const StatusHistoryPanel: React.FC = ({ }) => { const theme = useTheme2(); - const { frames, warn } = useMemo(() => prepareTimelineFields(data?.series, false), [data]); + const { frames, warn } = useMemo(() => prepareTimelineFields(data?.series, false, theme), [data, theme]); const legendItems = useMemo(() => prepareTimelineLegendItems(frames, options.legend, theme), [ frames,