Sparkline: Properly handle flat data (min === max) (#109702)
This commit is contained in:
@@ -26,7 +26,7 @@ import { UPlotChart } from '../uPlot/Plot';
|
||||
import { UPlotConfigBuilder } from '../uPlot/config/UPlotConfigBuilder';
|
||||
import { preparePlotData2, getStackingGroups } from '../uPlot/utils';
|
||||
|
||||
import { preparePlotFrame } from './utils';
|
||||
import { getYRange, preparePlotFrame } from './utils';
|
||||
|
||||
export interface SparklineProps extends Themeable2 {
|
||||
width: number;
|
||||
@@ -102,26 +102,7 @@ export class Sparkline extends PureComponent<SparklineProps, State> {
|
||||
}
|
||||
|
||||
getYRange(field: Field): Range.MinMax {
|
||||
let { min, max } = this.state.alignedDataFrame.fields[1].state?.range!;
|
||||
const noValue = +this.state.alignedDataFrame.fields[1].config?.noValue!;
|
||||
|
||||
if (!Number.isNaN(noValue)) {
|
||||
min = Math.min(min!, +noValue);
|
||||
max = Math.max(max!, +noValue);
|
||||
}
|
||||
|
||||
if (min === max) {
|
||||
if (min === 0) {
|
||||
max = 100;
|
||||
} else {
|
||||
min = 0;
|
||||
max! *= 2;
|
||||
}
|
||||
|
||||
return [min, max!];
|
||||
}
|
||||
|
||||
return [Math.max(min!, field.config.min ?? -Infinity), Math.min(max!, field.config.max ?? Infinity)];
|
||||
return getYRange(field, this.state.alignedDataFrame);
|
||||
}
|
||||
|
||||
prepareConfig(data: DataFrame) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { FieldSparkline, FieldType } from '@grafana/data';
|
||||
import { Field, FieldSparkline, FieldType } from '@grafana/data';
|
||||
|
||||
import { preparePlotFrame } from './utils';
|
||||
import { getYRange, preparePlotFrame } from './utils';
|
||||
|
||||
describe('Prepare Sparkline plot frame', () => {
|
||||
it('should return sorted array if x-axis numeric', () => {
|
||||
@@ -91,3 +91,89 @@ describe('Prepare Sparkline plot frame', () => {
|
||||
expect(frame.fields[1].values).toEqual([2, null, 3, null, null, 1]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Get y range', () => {
|
||||
const defaultYField: Field = {
|
||||
name: 'y',
|
||||
values: [1, 2, 3, 4, 5],
|
||||
type: FieldType.number,
|
||||
config: {},
|
||||
state: { range: { min: 1, max: 5, delta: 4 } },
|
||||
};
|
||||
const straightLineYField: Field = {
|
||||
name: 'y',
|
||||
values: [2, 2, 2, 2, 2],
|
||||
type: FieldType.number,
|
||||
config: {},
|
||||
state: { range: { min: 2, max: 2, delta: 0 } },
|
||||
};
|
||||
const straightLineNegYField: Field = {
|
||||
name: 'y',
|
||||
values: [-2, -2, -2, -2, -2],
|
||||
type: FieldType.number,
|
||||
config: {},
|
||||
state: { range: { min: -2, max: -2, delta: 0 } },
|
||||
};
|
||||
const xField: Field = {
|
||||
name: 'x',
|
||||
values: [1000, 2000, 3000, 4000, 5000],
|
||||
type: FieldType.time,
|
||||
config: {},
|
||||
};
|
||||
const getAlignedFrame = (yField: Field) => ({
|
||||
refId: 'sparkline',
|
||||
fields: [xField, yField],
|
||||
length: yField.values.length,
|
||||
});
|
||||
it.each([
|
||||
{
|
||||
description: 'inferred min and max',
|
||||
field: defaultYField,
|
||||
expected: [1, 5],
|
||||
},
|
||||
{
|
||||
description: 'min from config',
|
||||
field: { ...defaultYField, config: { min: 3 }, state: { range: { min: 3, max: 5, delta: 2 } } },
|
||||
expected: [3, 5],
|
||||
},
|
||||
{
|
||||
description: 'max from config',
|
||||
field: { ...defaultYField, config: { max: 30 }, state: { range: { min: 1, max: 30, delta: 29 } } },
|
||||
expected: [1, 30],
|
||||
},
|
||||
{
|
||||
description: 'no value is set',
|
||||
field: { ...defaultYField, config: { noValue: '0' } },
|
||||
expected: [0, 5],
|
||||
},
|
||||
{
|
||||
description: 'NaN no value is set',
|
||||
field: { ...defaultYField, config: { noValue: 'foo' } },
|
||||
expected: [1, 5],
|
||||
},
|
||||
{
|
||||
description: 'straight line',
|
||||
field: straightLineYField,
|
||||
expected: [0, 4],
|
||||
},
|
||||
{
|
||||
description: 'straight line, negative values',
|
||||
field: straightLineNegYField,
|
||||
expected: [-4, 0],
|
||||
},
|
||||
{
|
||||
description: 'straight line with config min and max',
|
||||
field: { ...straightLineYField, config: { min: 1, max: 3 }, state: { range: { min: 1, max: 3, delta: 2 } } },
|
||||
expected: [1, 3],
|
||||
},
|
||||
{
|
||||
description: 'straight line with config no value',
|
||||
field: { ...straightLineYField, config: { noValue: '0' } },
|
||||
expected: [0, 2],
|
||||
},
|
||||
])(`should return correct range for $description`, ({ field, expected }) => {
|
||||
const actual = getYRange(field, getAlignedFrame(field));
|
||||
expect(actual).toEqual(expected);
|
||||
expect(actual[0]).toBeLessThan(actual[1]!);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { Range } from 'uplot';
|
||||
|
||||
import {
|
||||
DataFrame,
|
||||
FieldConfig,
|
||||
@@ -6,6 +8,7 @@ import {
|
||||
isLikelyAscendingVector,
|
||||
sortDataFrame,
|
||||
applyNullInsertThreshold,
|
||||
Field,
|
||||
} from '@grafana/data';
|
||||
import { GraphFieldConfig } from '@grafana/schema';
|
||||
|
||||
@@ -48,3 +51,37 @@ export function preparePlotFrame(sparkline: FieldSparkline, config?: FieldConfig
|
||||
refFieldPseudoMax: sparkline.timeRange?.to.valueOf(),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* apply configuration defaults and ensure that the range is never two equal values.
|
||||
*/
|
||||
export function getYRange(field: Field, alignedFrame: DataFrame): Range.MinMax {
|
||||
let { min, max } = alignedFrame.fields[1].state?.range!;
|
||||
|
||||
// enure that the min/max from the field config are respected
|
||||
min = Math.max(min!, field.config.min ?? -Infinity);
|
||||
max = Math.min(max!, field.config.max ?? Infinity);
|
||||
|
||||
// if noValue is set, ensure that it is included in the range as well
|
||||
const noValue = +alignedFrame.fields[1].config?.noValue!;
|
||||
if (!Number.isNaN(noValue)) {
|
||||
min = Math.min(min, noValue);
|
||||
max = Math.max(max, noValue);
|
||||
}
|
||||
|
||||
// if min and max are equal after all of that, create a range
|
||||
// that allows the sparkline to be visible in the center of the viz
|
||||
if (min === max) {
|
||||
if (min === 0) {
|
||||
max = 100;
|
||||
} else if (min < 0) {
|
||||
max = 0;
|
||||
min *= 2;
|
||||
} else {
|
||||
min = 0;
|
||||
max *= 2;
|
||||
}
|
||||
}
|
||||
|
||||
return [min, max];
|
||||
}
|
||||
|
||||
@@ -97,6 +97,7 @@ export function TableNG(props: TableNGProps) {
|
||||
onSortByChange,
|
||||
showTypeIcons,
|
||||
structureRev,
|
||||
timeRange,
|
||||
transparent,
|
||||
width,
|
||||
} = props;
|
||||
@@ -461,6 +462,7 @@ export function TableNG(props: TableNGProps) {
|
||||
theme,
|
||||
value,
|
||||
width,
|
||||
timeRange,
|
||||
cellInspect,
|
||||
showFilters,
|
||||
getActions: getCellActions,
|
||||
@@ -646,6 +648,7 @@ export function TableNG(props: TableNGProps) {
|
||||
showTypeIcons,
|
||||
sortColumns,
|
||||
styles,
|
||||
timeRange,
|
||||
theme,
|
||||
visibleFields,
|
||||
widths,
|
||||
|
||||
Reference in New Issue
Block a user