StateTimeline: merge threshold values (#35073)

This commit is contained in:
Ryan McKinley 2021-09-01 08:43:57 -07:00 committed by GitHub
parent 7fe3599ab1
commit e980f8531a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 121 additions and 48 deletions

View File

@ -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 <div></div>; // don't show anything in the legend
}
const items = getThresholdItems(color.field!.config, config.theme2);
return (
<div className={style.infoWrap}>
{thresholds && (
<div className={style.legend}>
{thresholds.steps.map((step: any, idx: number) => {
const next = thresholds!.steps[idx + 1];
let info = <span>?</span>;
if (idx === 0) {
info = <span>&lt; {fmt(next.value)}</span>;
} else if (next) {
info = (
<span>
{fmt(step.value)} - {fmt(next.value)}
</span>
);
} else {
info = <span>{fmt(step.value)} +</span>;
}
return (
<div key={`${idx}/${step.value}`} className={style.legendItem}>
<i style={{ background: config.theme2.visualization.getColorByName(step.color) }}></i>
{info}
</div>
);
})}
</div>
)}
<div className={style.legend}>
{items.map((item: VizLegendItem, idx: number) => (
<div key={`${idx}/${item.label}`} className={style.legendItem}>
<i style={{ background: item.color }}></i>
{item.label}
</div>
))}
</div>
</div>
);
}

View File

@ -23,9 +23,10 @@ export const StateTimelinePanel: React.FC<TimelinePanelProps> = ({
}) => {
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), [

View File

@ -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];

View File

@ -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<Threshold, string>();
const textToColor = new Map<string, string>();
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<String | undefined>(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

View File

@ -22,7 +22,7 @@ export const StatusHistoryPanel: React.FC<TimelinePanelProps> = ({
}) => {
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,