diff --git a/public/app/plugins/panel/barchart/BarChartPanel.tsx b/public/app/plugins/panel/barchart/BarChartPanel.tsx index 885e07ad6f9..0a4f9ef9869 100755 --- a/public/app/plugins/panel/barchart/BarChartPanel.tsx +++ b/public/app/plugins/panel/barchart/BarChartPanel.tsx @@ -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 {} @@ -10,6 +11,7 @@ interface Props extends PanelProps {} * @alpha */ export const BarChartPanel: React.FunctionComponent = ({ 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 = ({ data, options, w return options.orientation; }, [width, height, options.orientation]); - if (!data || !data.series?.length) { + if (!frames || warn) { return (
-

No data found in response

-
- ); - } - - const firstFrame = data.series[0]; - if (!firstFrame.fields.some((f) => f.type === FieldType.string)) { - return ( -
-

Bar charts requires a string field

-
- ); - } - if (!firstFrame.fields.some((f) => f.type === FieldType.number)) { - return ( -
-

No numeric fields found

+

{warn ?? 'No data found in response'}

); } return ( { ).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, + ] + `); + }); + }); }); diff --git a/public/app/plugins/panel/barchart/utils.ts b/public/app/plugins/panel/barchart/utils.ts index 9c39f0d849c..e550439f519 100644 --- a/public/app/plugins/panel/barchart/utils.ts +++ b/public/app/plugins/panel/barchart/utils.ts @@ -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 }; +} diff --git a/public/app/plugins/panel/timeseries/utils.test.ts b/public/app/plugins/panel/timeseries/utils.test.ts index e0c744ba7db..a1544dd9297 100644 --- a/public/app/plugins/panel/timeseries/utils.test.ts +++ b/public/app/plugins/panel/timeseries/utils.test.ts @@ -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, + ] + `); + }); }); diff --git a/public/app/plugins/panel/timeseries/utils.ts b/public/app/plugins/panel/timeseries/utils.ts index 71795ebb8ec..702f1227be3 100644 --- a/public/app/plugins/panel/timeseries/utils.ts +++ b/public/app/plugins/panel/timeseries/utils.ts @@ -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) {