Timeseries: support boolean values out-of-the-box (#34168)

This commit is contained in:
Ryan McKinley
2021-05-17 09:52:47 -07:00
committed by GitHub
parent 331991ca10
commit 7b32c5439b
9 changed files with 222 additions and 32 deletions

View File

@@ -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}

View 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",
}
`);
});
});

View 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 };
}