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,