Sparkline: Improve min/max logic to avoid issues for very narrow deltas (#115030)

* Sparkline: Prevent infinite loop when rendering a sparkline with a single value

* some tests for this case

* refactor out utils, experiment with getting highlightIndex working

* add comments throughout for #112977

* remove unused import

* Update Sparkline.test.tsx

* fix points mode rendering

* Sparkline: Improve min/max logic to avoid issues for very narrow deltas

* spread all config

* defaults deep

* delete unused import

* remove go.work.sum delta

* line break at end of file
This commit is contained in:
Paul Marbach
2025-12-10 16:54:29 -05:00
committed by GitHub
parent e433cfa02d
commit 000c00aee9
4 changed files with 96 additions and 30 deletions
@@ -114,6 +114,13 @@ describe('Get y range', () => {
config: {},
state: { range: { min: -2, max: -2, delta: 0 } },
};
const decimalsCloseYField: Field = {
name: 'y',
values: [2, 1.999999999999999, 2.000000000000001, 2, 2],
type: FieldType.number,
config: {},
state: { range: { min: 1.999999999999999, max: 2.000000000000001, delta: 0 } },
};
const xField: Field = {
name: 'x',
values: [1000, 2000, 3000, 4000, 5000],
@@ -154,12 +161,12 @@ describe('Get y range', () => {
{
description: 'straight line',
field: straightLineYField,
expected: [0, 4],
expected: [2, 4],
},
{
description: 'straight line, negative values',
field: straightLineNegYField,
expected: [-4, 0],
expected: [-4, -2],
},
{
description: 'straight line with config min and max',
@@ -171,8 +178,13 @@ describe('Get y range', () => {
field: { ...straightLineYField, config: { noValue: '0' } },
expected: [0, 2],
},
{
description: 'long decimals which are nearly equal and result in a functional delta of 0',
field: decimalsCloseYField,
expected: [2, 4],
},
])(`should return correct range for $description`, ({ field, expected }) => {
const actual = getYRange(field, getAlignedFrame(field));
const actual = getYRange(getAlignedFrame(field));
expect(actual).toEqual(expected);
expect(actual[0]).toBeLessThan(actual[1]!);
});
@@ -3,7 +3,6 @@ import { Range } from 'uplot';
import {
applyNullInsertThreshold,
DataFrame,
Field,
FieldConfig,
FieldSparkline,
FieldType,
@@ -11,6 +10,7 @@ import {
GrafanaTheme2,
isLikelyAscendingVector,
nullToValue,
roundDecimals,
sortDataFrame,
} from '@grafana/data';
import { t } from '@grafana/i18n';
@@ -68,35 +68,49 @@ export function preparePlotFrame(sparkline: FieldSparkline, config?: FieldConfig
/**
* 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!;
export function getYRange(alignedFrame: DataFrame): Range.MinMax {
const field = alignedFrame.fields[1];
let { min, max } = field.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);
// enure that the min/max from the field config are respected.
min = Math.min(min!, field.config.min ?? Infinity);
max = Math.max(max!, field.config.max ?? -Infinity);
// console.log({ min, max });
// if noValue is set, ensure that it is included in the range as well
const noValue = +alignedFrame.fields[1].config?.noValue!;
const noValue = +field.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;
}
// call roundDecimals to mirror what is going to eventually happen in uplot
let roundedMin = roundDecimals(min, field.config.decimals ?? 0);
let roundedMax = roundDecimals(max, field.config.decimals ?? 0);
// if the rounded min and max are different,
// we can return the real min and max.
if (roundedMin !== roundedMax) {
return [min, max];
}
return [min, max];
// we are forced to tweak the min and max since they
// will be treated as equal after rounding by uPlot.
if (roundedMin === 0) {
// both are zero
roundedMax = 1;
} else if (roundedMin < 0) {
// both are negative
// max = 0;
roundedMin *= 2;
} else {
// both are positive
// min = 0;
roundedMax *= 2;
}
return [roundedMin, roundedMax];
}
// TODO: #112977 enable highlight index
@@ -182,7 +196,7 @@ export const prepareConfig = (
scaleKey,
orientation: ScaleOrientation.Vertical,
direction: ScaleDirection.Up,
range: () => getYRange(field, dataFrame),
range: () => getYRange(dataFrame),
});
builder.addAxis({