Skip to content

Conversation

@fastfrwrd
Copy link

We have had some instances recently where the min and max being equal crashes the browser in Grafana:

While we should not attempt to remediate this inside uPlot, it would be better to just throw if we can tell that's about to happen rather than running out of memory.

if (scaleMin === scaleMax)
throw new RangeError("scaleMin cannot be equal to scaleMax");

if (roundDec(scaleMin + foundIncr, numDec) === scaleMin)
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the only sus thing from the perspective of performance I think. in reality, it's one extra round beyond the rounding the method already does as part of the below for loop, so it seems negligible for the benefit.

@leeoniya
Copy link
Owner

hey paul, nice to see you :)

While we should not attempt to remediate this inside uPlot

since uPlot has to handle flat data (e.g. single data points), there is already min ~= max remediation in the default scale ranging functions, e.g.:

uPlot/src/utils.js

Lines 206 to 271 in 3a56bc6

function _rangeNum(_min, _max, cfg) {
let cmin = cfg.min;
let cmax = cfg.max;
let padMin = ifNull(cmin.pad, 0);
let padMax = ifNull(cmax.pad, 0);
let hardMin = ifNull(cmin.hard, -inf);
let hardMax = ifNull(cmax.hard, inf);
let softMin = ifNull(cmin.soft, inf);
let softMax = ifNull(cmax.soft, -inf);
let softMinMode = ifNull(cmin.mode, 0);
let softMaxMode = ifNull(cmax.mode, 0);
let delta = _max - _min;
let deltaMag = log10(delta);
let scalarMax = max(abs(_min), abs(_max));
let scalarMag = log10(scalarMax);
let scalarMagDelta = abs(scalarMag - deltaMag);
// this handles situations like 89.7, 89.69999999999999
// by assuming 0.001x deltas are precision errors
// if (delta > 0 && delta < abs(_max) / 1e3)
// delta = 0;
// treat data as flat if delta is less than 1e-24
// or range is 11+ orders of magnitude below raw values, e.g. 99999999.99999996 - 100000000.00000004
if (delta < 1e-24 || scalarMagDelta > 10) {
delta = 0;
// if soft mode is 2 and all vals are flat at 0, avoid the 0.1 * 1e3 fallback
// this prevents 0,0,0 from ranging to -100,100 when softMin/softMax are -1,1
if (_min == 0 || _max == 0) {
delta = 1e-24;
if (softMinMode == 2 && softMin != inf)
padMin = 0;
if (softMaxMode == 2 && softMax != -inf)
padMax = 0;
}
}
let nonZeroDelta = delta || scalarMax || 1e3;
let mag = log10(nonZeroDelta);
let base = pow(10, floor(mag));
let _padMin = nonZeroDelta * (delta == 0 ? (_min == 0 ? .1 : 1) : padMin);
let _newMin = roundDec(incrRoundDn(_min - _padMin, base/10), 24);
let _softMin = _min >= softMin && (softMinMode == 1 || softMinMode == 3 && _newMin <= softMin || softMinMode == 2 && _newMin >= softMin) ? softMin : inf;
let minLim = max(hardMin, _newMin < _softMin && _min >= _softMin ? _softMin : min(_softMin, _newMin));
let _padMax = nonZeroDelta * (delta == 0 ? (_max == 0 ? .1 : 1) : padMax);
let _newMax = roundDec(incrRoundUp(_max + _padMax, base/10), 24);
let _softMax = _max <= softMax && (softMaxMode == 1 || softMaxMode == 3 && _newMax >= softMax || softMaxMode == 2 && _newMax <= softMax) ? softMax : -inf;
let maxLim = min(hardMax, _newMax > _softMax && _max <= _softMax ? _softMax : max(_softMax, _newMax));
if (minLim == maxLim && minLim == 0)
maxLim = 100;
return [minLim, maxLim];
}

this is why splits finding functions [and everything downstream from scale.range] expect that min ~= max situation is already handled early and in one place. is the situation that in Grafana we're customizing the scale.range option and not doing the same checks?

the example data in the added test seems to work okay with the built-in ranger: https://jsfiddle.net/z0xaq398/

let opts = {
  width: 400,
  height: 170,
  scales: {
    x: {
      time: false,
    },
  },
  series: [
    {},
    {
      stroke: "red",
    },
  ],
};

let data = [
  [0, 1, 2, 3, 4],
  [2, 1.999999999999999, 2.000000000000001, 2, 2],
];

let u = new uPlot(opts, data, document.body);

while i agree that crashing and infinite loop is not a great outcome, it should be the responsibility of the customized options to uphold the same contract as the built-in functions (the ranging util functions are also statically exported for this purpose). i wonder if uPlot should additionally expose some finer-grained helpers to avoid too much copy-paste into any customized scale.range callbacks. and/or provide some kind of conformance tests that allow someone to validate that their own range functions handle all the same situations that uPlot's default ones already handle.

@fastfrwrd
Copy link
Author

is the situation that in Grafana we're customizing the scale.range option and not doing the same checks?

Yeah, that's exactly right. The Sparkline is doing its own range configuration, mainly because it's user configurable in some situations like Table, and we keep running into cases where this happens. I wonder if there's a way to do the kind of check we do for auto-range to a configured range, and then maybe adjust what was provided if it's going to be a problem.

i wonder if uPlot should additionally expose some finer-grained helpers to avoid too much copy-paste into any customized scale.range callbacks

I like that idea!

@fastfrwrd
Copy link
Author

fastfrwrd commented Dec 12, 2025

one util that would be nice to have would be some way to get the implied decimal value, if we have to do this rounding out in Grafana ourselves, we could really use a way to know what precision uPlot is gonna round by if no decimal is sent in so we can get the same rounded values out. (I believe that's guessDecimals in grafana but would be better to call uPlot to make sure that we're always in sync)

@leeoniya
Copy link
Owner

@fastfrwrd could you minimally extend the above jsfiddle to show how an exported decimals lookup would be used in a custom scale.range? i guess with the currently-problematic values?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants