mirror of
https://github.com/grafana/grafana.git
synced 2024-11-22 08:56:43 -06:00
StateTimeline: merge threshold values (#35073)
This commit is contained in:
parent
7fe3599ab1
commit
e980f8531a
@ -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>< {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>
|
||||
);
|
||||
}
|
||||
|
@ -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), [
|
||||
|
@ -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];
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
Loading…
Reference in New Issue
Block a user