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 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 { formattedValueToString, getFieldColorModeForField, GrafanaTheme2 } from '@grafana/data';
|
||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
import { config } from 'app/core/config';
|
import { config } from 'app/core/config';
|
||||||
import { DimensionSupplier } from 'app/features/dimensions';
|
import { DimensionSupplier } from 'app/features/dimensions';
|
||||||
|
import { getThresholdItems } from 'app/plugins/panel/state-timeline/utils';
|
||||||
import { getMinMaxAndDelta } from '../../../../../../../packages/grafana-data/src/field/scale';
|
import { getMinMaxAndDelta } from '../../../../../../../packages/grafana-data/src/field/scale';
|
||||||
|
|
||||||
export interface MarkersLegendProps {
|
export interface MarkersLegendProps {
|
||||||
@ -56,33 +57,17 @@ export function MarkersLegend(props: MarkersLegendProps) {
|
|||||||
return <div></div>; // don't show anything in the legend
|
return <div></div>; // don't show anything in the legend
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const items = getThresholdItems(color.field!.config, config.theme2);
|
||||||
return (
|
return (
|
||||||
<div className={style.infoWrap}>
|
<div className={style.infoWrap}>
|
||||||
{thresholds && (
|
<div className={style.legend}>
|
||||||
<div className={style.legend}>
|
{items.map((item: VizLegendItem, idx: number) => (
|
||||||
{thresholds.steps.map((step: any, idx: number) => {
|
<div key={`${idx}/${item.label}`} className={style.legendItem}>
|
||||||
const next = thresholds!.steps[idx + 1];
|
<i style={{ background: item.color }}></i>
|
||||||
let info = <span>?</span>;
|
{item.label}
|
||||||
if (idx === 0) {
|
</div>
|
||||||
info = <span>< {fmt(next.value)}</span>;
|
))}
|
||||||
} else if (next) {
|
</div>
|
||||||
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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -23,9 +23,10 @@ export const StateTimelinePanel: React.FC<TimelinePanelProps> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const theme = useTheme2();
|
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,
|
data,
|
||||||
options.mergeValues,
|
options.mergeValues,
|
||||||
|
theme,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const legendItems = useMemo(() => prepareTimelineLegendItems(frames, options.legend, 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';
|
import { findNextStateIndex, prepareTimelineFields } from './utils';
|
||||||
|
|
||||||
|
const theme = createTheme();
|
||||||
|
|
||||||
describe('prepare timeline graph', () => {
|
describe('prepare timeline graph', () => {
|
||||||
it('errors with no time fields', () => {
|
it('errors with no time fields', () => {
|
||||||
const frames = [
|
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');
|
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');
|
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();
|
expect(info.warn).toBeUndefined();
|
||||||
|
|
||||||
const out = info.frames![0];
|
const out = info.frames![0];
|
||||||
|
@ -12,6 +12,9 @@ import {
|
|||||||
getFieldDisplayName,
|
getFieldDisplayName,
|
||||||
getValueFormat,
|
getValueFormat,
|
||||||
GrafanaTheme2,
|
GrafanaTheme2,
|
||||||
|
getActiveThreshold,
|
||||||
|
Threshold,
|
||||||
|
getFieldConfigWithMinMax,
|
||||||
outerJoinDataFrames,
|
outerJoinDataFrames,
|
||||||
ThresholdsMode,
|
ThresholdsMode,
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
@ -257,10 +260,73 @@ export function unsetSameFutureValues(values: any[]): any[] | undefined {
|
|||||||
return clone;
|
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
|
// This will return a set of frames with only graphable values included
|
||||||
export function prepareTimelineFields(
|
export function prepareTimelineFields(
|
||||||
series: DataFrame[] | undefined,
|
series: DataFrame[] | undefined,
|
||||||
mergeValues: boolean
|
mergeValues: boolean,
|
||||||
|
theme: GrafanaTheme2
|
||||||
): { frames?: DataFrame[]; warn?: string } {
|
): { frames?: DataFrame[]; warn?: string } {
|
||||||
if (!series?.length) {
|
if (!series?.length) {
|
||||||
return { warn: 'No data in response' };
|
return { warn: 'No data in response' };
|
||||||
@ -279,6 +345,15 @@ export function prepareTimelineFields(
|
|||||||
fields.push(field);
|
fields.push(field);
|
||||||
break;
|
break;
|
||||||
case FieldType.number:
|
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.boolean:
|
||||||
case FieldType.string:
|
case FieldType.string:
|
||||||
field = {
|
field = {
|
||||||
@ -332,6 +407,30 @@ export function prepareTimelineFields(
|
|||||||
return { frames };
|
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(
|
export function prepareTimelineLegendItems(
|
||||||
frames: DataFrame[] | undefined,
|
frames: DataFrame[] | undefined,
|
||||||
options: VizLegendOptions,
|
options: VizLegendOptions,
|
||||||
@ -353,21 +452,7 @@ export function prepareTimelineLegendItems(
|
|||||||
|
|
||||||
// If thresholds are enabled show each step in the legend
|
// If thresholds are enabled show each step in the legend
|
||||||
if (colorMode === FieldColorModeId.Thresholds && thresholds?.steps && thresholds.steps.length > 1) {
|
if (colorMode === FieldColorModeId.Thresholds && thresholds?.steps && thresholds.steps.length > 1) {
|
||||||
const steps = thresholds.steps;
|
return getThresholdItems(fieldConfig, theme);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// If thresholds are enabled show each step in the legend
|
// If thresholds are enabled show each step in the legend
|
||||||
|
@ -22,7 +22,7 @@ export const StatusHistoryPanel: React.FC<TimelinePanelProps> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const theme = useTheme2();
|
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), [
|
const legendItems = useMemo(() => prepareTimelineLegendItems(frames, options.legend, theme), [
|
||||||
frames,
|
frames,
|
||||||
|
Loading…
Reference in New Issue
Block a user