mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Timeseries: support boolean values out-of-the-box (#34168)
This commit is contained in:
@@ -1,11 +1,13 @@
|
||||
import { anySeriesWithTimeField, DashboardCursorSync, Field, PanelProps } from '@grafana/data';
|
||||
import { DashboardCursorSync, Field, PanelProps } from '@grafana/data';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { TooltipDisplayMode, usePanelContext, TimeSeries, TooltipPlugin, ZoomPlugin } from '@grafana/ui';
|
||||
import { getFieldLinksForExplore } from 'app/features/explore/utils/links';
|
||||
import React from 'react';
|
||||
import React, { useMemo } from 'react';
|
||||
import { AnnotationsPlugin } from './plugins/AnnotationsPlugin';
|
||||
import { ContextMenuPlugin } from './plugins/ContextMenuPlugin';
|
||||
import { ExemplarsPlugin } from './plugins/ExemplarsPlugin';
|
||||
import { TimeSeriesOptions } from './types';
|
||||
import { prepareGraphableFields } from './utils';
|
||||
|
||||
interface TimeSeriesPanelProps extends PanelProps<TimeSeriesOptions> {}
|
||||
|
||||
@@ -25,25 +27,19 @@ export const TimeSeriesPanel: React.FC<TimeSeriesPanelProps> = ({
|
||||
return getFieldLinksForExplore({ field, rowIndex, range: timeRange });
|
||||
};
|
||||
|
||||
if (!data || !data.series?.length) {
|
||||
return (
|
||||
<div className="panel-empty">
|
||||
<p>No data found in response</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const { frames, warn } = useMemo(() => prepareGraphableFields(data?.series, config.theme2), [data]);
|
||||
|
||||
if (!anySeriesWithTimeField(data.series)) {
|
||||
if (!frames || warn) {
|
||||
return (
|
||||
<div className="panel-empty">
|
||||
<p>Missing time field in the data</p>
|
||||
<p>{warn ?? 'No data found in response'}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<TimeSeries
|
||||
frames={data.series}
|
||||
frames={frames}
|
||||
structureRev={data.structureRev}
|
||||
timeRange={timeRange}
|
||||
timeZone={timeZone}
|
||||
|
||||
61
public/app/plugins/panel/timeseries/utils.test.ts
Normal file
61
public/app/plugins/panel/timeseries/utils.test.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import { createTheme, FieldType, toDataFrame } from '@grafana/data';
|
||||
import { prepareGraphableFields } from './utils';
|
||||
|
||||
describe('prepare timeseries graph', () => {
|
||||
it('errors with no time fields', () => {
|
||||
const frames = [
|
||||
toDataFrame({
|
||||
fields: [
|
||||
{ name: 'a', values: [1, 2, 3] },
|
||||
{ name: 'b', values: ['a', 'b', 'c'] },
|
||||
],
|
||||
}),
|
||||
];
|
||||
const info = prepareGraphableFields(frames, createTheme());
|
||||
expect(info.warn).toEqual('Data does not have a time field');
|
||||
});
|
||||
|
||||
it('requires a number or boolean value', () => {
|
||||
const frames = [
|
||||
toDataFrame({
|
||||
fields: [
|
||||
{ name: 'a', type: FieldType.time, values: [1, 2, 3] },
|
||||
{ name: 'b', values: ['a', 'b', 'c'] },
|
||||
],
|
||||
}),
|
||||
];
|
||||
const info = prepareGraphableFields(frames, createTheme());
|
||||
expect(info.warn).toEqual('No graphable fields');
|
||||
});
|
||||
|
||||
it('will graph numbers and boolean values', () => {
|
||||
const frames = [
|
||||
toDataFrame({
|
||||
fields: [
|
||||
{ name: 'a', type: FieldType.time, values: [1, 2, 3] },
|
||||
{ name: 'b', values: ['a', 'b', 'c'] },
|
||||
{ name: 'c', values: [true, false, true] },
|
||||
{ name: 'd', values: [100, 200, 300] },
|
||||
],
|
||||
}),
|
||||
];
|
||||
const info = prepareGraphableFields(frames, createTheme());
|
||||
expect(info.warn).toBeUndefined();
|
||||
|
||||
const out = info.frames![0];
|
||||
expect(out.fields.map((f) => f.name)).toEqual(['a', 'c', 'd']);
|
||||
|
||||
const field = out.fields.find((f) => f.name === 'c');
|
||||
expect(field?.display).toBeDefined();
|
||||
expect(field!.display!(1)).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"color": "#808080",
|
||||
"numeric": 1,
|
||||
"percent": 1,
|
||||
"prefix": undefined,
|
||||
"suffix": undefined,
|
||||
"text": "True",
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
||||
92
public/app/plugins/panel/timeseries/utils.ts
Normal file
92
public/app/plugins/panel/timeseries/utils.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import {
|
||||
ArrayVector,
|
||||
DataFrame,
|
||||
Field,
|
||||
FieldType,
|
||||
getDisplayProcessor,
|
||||
GrafanaTheme2,
|
||||
isBooleanUnit,
|
||||
} from '@grafana/data';
|
||||
import { GraphFieldConfig, LineInterpolation } from '@grafana/ui';
|
||||
|
||||
// This will return a set of frames with only graphable values included
|
||||
export function prepareGraphableFields(
|
||||
series: DataFrame[] | undefined,
|
||||
theme: GrafanaTheme2
|
||||
): { frames?: DataFrame[]; warn?: string } {
|
||||
if (!series?.length) {
|
||||
return { warn: 'No data in response' };
|
||||
}
|
||||
let hasTimeseries = false;
|
||||
const frames: DataFrame[] = [];
|
||||
for (let frame of series) {
|
||||
let isTimeseries = false;
|
||||
let changed = false;
|
||||
const fields: Field[] = [];
|
||||
for (const field of frame.fields) {
|
||||
switch (field.type) {
|
||||
case FieldType.time:
|
||||
isTimeseries = true;
|
||||
hasTimeseries = true;
|
||||
fields.push(field);
|
||||
break;
|
||||
case FieldType.number:
|
||||
fields.push(field);
|
||||
break; // ok
|
||||
case FieldType.boolean:
|
||||
changed = true;
|
||||
const custom: GraphFieldConfig = field.config?.custom ?? {};
|
||||
const config = {
|
||||
...field.config,
|
||||
max: 1,
|
||||
min: 0,
|
||||
custom,
|
||||
};
|
||||
// smooth and linear do not make sense
|
||||
if (custom.lineInterpolation !== LineInterpolation.StepBefore) {
|
||||
custom.lineInterpolation = LineInterpolation.StepAfter;
|
||||
}
|
||||
const copy = {
|
||||
...field,
|
||||
config,
|
||||
type: FieldType.number,
|
||||
values: new ArrayVector(
|
||||
field.values.toArray().map((v) => {
|
||||
if (v == null) {
|
||||
return v;
|
||||
}
|
||||
return Boolean(v) ? 1 : 0;
|
||||
})
|
||||
),
|
||||
};
|
||||
if (!isBooleanUnit(config.unit)) {
|
||||
config.unit = 'bool';
|
||||
copy.display = getDisplayProcessor({ field: copy, theme });
|
||||
}
|
||||
fields.push(copy);
|
||||
break;
|
||||
default:
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
if (isTimeseries && fields.length > 1) {
|
||||
hasTimeseries = true;
|
||||
if (changed) {
|
||||
frames.push({
|
||||
...frame,
|
||||
fields,
|
||||
});
|
||||
} else {
|
||||
frames.push(frame);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasTimeseries) {
|
||||
return { warn: 'Data does not have a time field' };
|
||||
}
|
||||
if (!frames.length) {
|
||||
return { warn: 'No graphable fields' };
|
||||
}
|
||||
return { frames };
|
||||
}
|
||||
Reference in New Issue
Block a user