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 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 { TooltipPlugin } from '@grafana/ui';
|
||||||
import { BarChartOptions } from './types';
|
import { BarChartOptions } from './types';
|
||||||
import { BarChart } from './BarChart';
|
import { BarChart } from './BarChart';
|
||||||
|
import { prepareGraphableFrames } from './utils';
|
||||||
|
|
||||||
interface Props extends PanelProps<BarChartOptions> {}
|
interface Props extends PanelProps<BarChartOptions> {}
|
||||||
|
|
||||||
@ -10,6 +11,7 @@ interface Props extends PanelProps<BarChartOptions> {}
|
|||||||
* @alpha
|
* @alpha
|
||||||
*/
|
*/
|
||||||
export const BarChartPanel: React.FunctionComponent<Props> = ({ data, options, width, height, timeZone }) => {
|
export const BarChartPanel: React.FunctionComponent<Props> = ({ data, options, width, height, timeZone }) => {
|
||||||
|
const { frames, warn } = useMemo(() => prepareGraphableFrames(data?.series), [data]);
|
||||||
const orientation = useMemo(() => {
|
const orientation = useMemo(() => {
|
||||||
if (!options.orientation || options.orientation === VizOrientation.Auto) {
|
if (!options.orientation || options.orientation === VizOrientation.Auto) {
|
||||||
return width < height ? VizOrientation.Horizontal : VizOrientation.Vertical;
|
return width < height ? VizOrientation.Horizontal : VizOrientation.Vertical;
|
||||||
@ -18,33 +20,17 @@ export const BarChartPanel: React.FunctionComponent<Props> = ({ data, options, w
|
|||||||
return options.orientation;
|
return options.orientation;
|
||||||
}, [width, height, options.orientation]);
|
}, [width, height, options.orientation]);
|
||||||
|
|
||||||
if (!data || !data.series?.length) {
|
if (!frames || warn) {
|
||||||
return (
|
return (
|
||||||
<div className="panel-empty">
|
<div className="panel-empty">
|
||||||
<p>No data found in response</p>
|
<p>{warn ?? '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>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BarChart
|
<BarChart
|
||||||
frames={data.series}
|
frames={frames}
|
||||||
timeZone={timeZone}
|
timeZone={timeZone}
|
||||||
timeRange={({ from: 1, to: 1 } as unknown) as TimeRange} // HACK
|
timeRange={({ from: 1, to: 1 } as unknown) as TimeRange} // HACK
|
||||||
structureRev={data.structureRev}
|
structureRev={data.structureRev}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { preparePlotConfigBuilder, preparePlotFrame } from './utils';
|
import { prepareGraphableFrames, preparePlotConfigBuilder, preparePlotFrame } from './utils';
|
||||||
import {
|
import {
|
||||||
createTheme,
|
createTheme,
|
||||||
DefaultTimeZone,
|
DefaultTimeZone,
|
||||||
@ -140,4 +140,56 @@ describe('BarChart utils', () => {
|
|||||||
).toMatchSnapshot();
|
).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 {
|
import {
|
||||||
|
ArrayVector,
|
||||||
DataFrame,
|
DataFrame,
|
||||||
|
Field,
|
||||||
FieldType,
|
FieldType,
|
||||||
formattedValueToString,
|
formattedValueToString,
|
||||||
getFieldColorModeForField,
|
getFieldColorModeForField,
|
||||||
@ -196,3 +198,54 @@ export function preparePlotFrame(data: DataFrame[]) {
|
|||||||
|
|
||||||
return resultFrame;
|
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';
|
import { prepareGraphableFields } from './utils';
|
||||||
|
|
||||||
describe('prepare timeseries graph', () => {
|
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) {
|
if (!series?.length) {
|
||||||
return { warn: 'No data in response' };
|
return { warn: 'No data in response' };
|
||||||
}
|
}
|
||||||
|
let copy: Field;
|
||||||
let hasTimeseries = false;
|
let hasTimeseries = false;
|
||||||
const frames: DataFrame[] = [];
|
const frames: DataFrame[] = [];
|
||||||
|
|
||||||
for (let frame of series) {
|
for (let frame of series) {
|
||||||
let isTimeseries = false;
|
let isTimeseries = false;
|
||||||
let changed = false;
|
let changed = false;
|
||||||
const fields: Field[] = [];
|
const fields: Field[] = [];
|
||||||
|
|
||||||
for (const field of frame.fields) {
|
for (const field of frame.fields) {
|
||||||
switch (field.type) {
|
switch (field.type) {
|
||||||
case FieldType.time:
|
case FieldType.time:
|
||||||
@ -31,7 +34,19 @@ export function prepareGraphableFields(
|
|||||||
fields.push(field);
|
fields.push(field);
|
||||||
break;
|
break;
|
||||||
case FieldType.number:
|
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
|
break; // ok
|
||||||
case FieldType.boolean:
|
case FieldType.boolean:
|
||||||
changed = true;
|
changed = true;
|
||||||
@ -46,7 +61,7 @@ export function prepareGraphableFields(
|
|||||||
if (custom.lineInterpolation !== LineInterpolation.StepBefore) {
|
if (custom.lineInterpolation !== LineInterpolation.StepBefore) {
|
||||||
custom.lineInterpolation = LineInterpolation.StepAfter;
|
custom.lineInterpolation = LineInterpolation.StepAfter;
|
||||||
}
|
}
|
||||||
const copy = {
|
copy = {
|
||||||
...field,
|
...field,
|
||||||
config,
|
config,
|
||||||
type: FieldType.number,
|
type: FieldType.number,
|
||||||
@ -69,6 +84,7 @@ export function prepareGraphableFields(
|
|||||||
changed = true;
|
changed = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isTimeseries && fields.length > 1) {
|
if (isTimeseries && fields.length > 1) {
|
||||||
hasTimeseries = true;
|
hasTimeseries = true;
|
||||||
if (changed) {
|
if (changed) {
|
||||||
|
Loading…
Reference in New Issue
Block a user