GraphNG: Handle infinite numbers as nulls when converting to plot array (#35638)

This commit is contained in:
Dominik Prokop 2021-06-15 22:00:09 +02:00 committed by GitHub
parent 368637c35a
commit 60f5865ee2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 152 additions and 24 deletions

View File

@ -1,8 +1,9 @@
import React, { useMemo } from 'react';
import { FieldType, PanelProps, TimeRange, VizOrientation } from '@grafana/data';
import { PanelProps, TimeRange, VizOrientation } from '@grafana/data';
import { TooltipPlugin } from '@grafana/ui';
import { BarChartOptions } from './types';
import { BarChart } from './BarChart';
import { prepareGraphableFrames } from './utils';
interface Props extends PanelProps<BarChartOptions> {}
@ -10,6 +11,7 @@ interface Props extends PanelProps<BarChartOptions> {}
* @alpha
*/
export const BarChartPanel: React.FunctionComponent<Props> = ({ data, options, width, height, timeZone }) => {
const { frames, warn } = useMemo(() => prepareGraphableFrames(data?.series), [data]);
const orientation = useMemo(() => {
if (!options.orientation || options.orientation === VizOrientation.Auto) {
return width < height ? VizOrientation.Horizontal : VizOrientation.Vertical;
@ -18,33 +20,17 @@ export const BarChartPanel: React.FunctionComponent<Props> = ({ data, options, w
return options.orientation;
}, [width, height, options.orientation]);
if (!data || !data.series?.length) {
if (!frames || warn) {
return (
<div className="panel-empty">
<p>No data found in response</p>
</div>
);
}
const firstFrame = data.series[0];
if (!firstFrame.fields.some((f) => f.type === FieldType.string)) {
return (
<div className="panel-empty">
<p>Bar charts requires a string field</p>
</div>
);
}
if (!firstFrame.fields.some((f) => f.type === FieldType.number)) {
return (
<div className="panel-empty">
<p>No numeric fields found</p>
<p>{warn ?? 'No data found in response'}</p>
</div>
);
}
return (
<BarChart
frames={data.series}
frames={frames}
timeZone={timeZone}
timeRange={({ from: 1, to: 1 } as unknown) as TimeRange} // HACK
structureRev={data.structureRev}

View File

@ -1,4 +1,4 @@
import { preparePlotConfigBuilder, preparePlotFrame } from './utils';
import { prepareGraphableFrames, preparePlotConfigBuilder, preparePlotFrame } from './utils';
import {
createTheme,
DefaultTimeZone,
@ -140,4 +140,56 @@ describe('BarChart utils', () => {
).toMatchSnapshot();
});
});
describe('prepareGraphableFrames', () => {
it('will warn when there is no data in the response', () => {
const result = prepareGraphableFrames([]);
expect(result.warn).toEqual('No data in response');
});
it('will warn when there is no string field in the response', () => {
const df = new MutableDataFrame({
fields: [
{ name: 'a', type: FieldType.time, values: [1, 2, 3, 4, 5] },
{ name: 'value', values: [1, 2, 3, 4, 5] },
],
});
const result = prepareGraphableFrames([df]);
expect(result.warn).toEqual('Bar charts requires a string field');
expect(result.frames).toBeUndefined();
});
it('will warn when there are no numeric fields in the response', () => {
const df = new MutableDataFrame({
fields: [
{ name: 'a', type: FieldType.string, values: ['a', 'b', 'c', 'd', 'e'] },
{ name: 'value', type: FieldType.boolean, values: [true, true, true, true, true] },
],
});
const result = prepareGraphableFrames([df]);
expect(result.warn).toEqual('No numeric fields found');
expect(result.frames).toBeUndefined();
});
it('will convert NaN and Infinty to nulls', () => {
const df = new MutableDataFrame({
fields: [
{ name: 'a', type: FieldType.string, values: ['a', 'b', 'c', 'd', 'e'] },
{ name: 'value', values: [-10, NaN, 10, -Infinity, +Infinity] },
],
});
const result = prepareGraphableFrames([df]);
const field = result.frames![0].fields[1];
expect(field!.values.toArray()).toMatchInlineSnapshot(`
Array [
-10,
null,
10,
null,
null,
]
`);
});
});
});

View File

@ -1,5 +1,7 @@
import {
ArrayVector,
DataFrame,
Field,
FieldType,
formattedValueToString,
getFieldColorModeForField,
@ -196,3 +198,54 @@ export function preparePlotFrame(data: DataFrame[]) {
return resultFrame;
}
/** @internal */
export function prepareGraphableFrames(series: DataFrame[]): { frames?: DataFrame[]; warn?: string } {
if (!series?.length) {
return { warn: 'No data in response' };
}
const frames: DataFrame[] = [];
const firstFrame = series[0];
if (!firstFrame.fields.some((f) => f.type === FieldType.string)) {
return {
warn: 'Bar charts requires a string field',
};
}
if (!firstFrame.fields.some((f) => f.type === FieldType.number)) {
return {
warn: 'No numeric fields found',
};
}
for (let frame of series) {
const fields: Field[] = [];
for (const field of frame.fields) {
if (field.type === FieldType.number) {
let copy = {
...field,
values: new ArrayVector(
field.values.toArray().map((v) => {
if (!(Number.isFinite(v) || v == null)) {
return null;
}
return v;
})
),
};
fields.push(copy);
} else {
fields.push({ ...field });
}
}
frames.push({
...frame,
fields,
});
}
return { frames };
}

View File

@ -1,4 +1,4 @@
import { createTheme, FieldType, toDataFrame } from '@grafana/data';
import { createTheme, FieldType, MutableDataFrame, toDataFrame } from '@grafana/data';
import { prepareGraphableFields } from './utils';
describe('prepare timeseries graph', () => {
@ -58,4 +58,25 @@ describe('prepare timeseries graph', () => {
}
`);
});
it('will convert NaN and Infinty to nulls', () => {
const df = new MutableDataFrame({
fields: [
{ name: 'time', type: FieldType.time, values: [995, 9996, 9997, 9998, 9999] },
{ name: 'a', values: [-10, NaN, 10, -Infinity, +Infinity] },
],
});
const result = prepareGraphableFields([df], createTheme());
const field = result.frames![0].fields.find((f) => f.name === 'a');
expect(field!.values.toArray()).toMatchInlineSnapshot(`
Array [
-10,
null,
10,
null,
null,
]
`);
});
});

View File

@ -17,12 +17,15 @@ export function prepareGraphableFields(
if (!series?.length) {
return { warn: 'No data in response' };
}
let copy: Field;
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:
@ -31,7 +34,19 @@ export function prepareGraphableFields(
fields.push(field);
break;
case FieldType.number:
fields.push(field);
changed = true;
copy = {
...field,
values: new ArrayVector(
field.values.toArray().map((v) => {
if (!(Number.isFinite(v) || v == null)) {
return null;
}
return v;
})
),
};
fields.push(copy);
break; // ok
case FieldType.boolean:
changed = true;
@ -46,7 +61,7 @@ export function prepareGraphableFields(
if (custom.lineInterpolation !== LineInterpolation.StepBefore) {
custom.lineInterpolation = LineInterpolation.StepAfter;
}
const copy = {
copy = {
...field,
config,
type: FieldType.number,
@ -69,6 +84,7 @@ export function prepareGraphableFields(
changed = true;
}
}
if (isTimeseries && fields.length > 1) {
hasTimeseries = true;
if (changed) {