mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
GraphNG: Handle infinite numbers as nulls when converting to plot array (#35638)
This commit is contained in:
parent
368637c35a
commit
60f5865ee2
@ -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}
|
||||
|
@ -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,
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -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 };
|
||||
}
|
||||
|
@ -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,
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
@ -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) {
|
||||
|
Loading…
Reference in New Issue
Block a user