# BUG: vtkAxis::RecalculateTickSpacing() loops forever when the axis range is only a few ULPs wide

**URL:** https://discourse.vtk.org/t/bug-vtkaxis-recalculatetickspacing-loops-forever-when-the-axis-range-is-only-a-few-ulps-wide/16545
**Category:** Support
**Created:** [September 29, 2026, 12:08pm UTC](https://discourse.vtk.org/t/bug-vtkaxis-recalculatetickspacing-loops-forever-when-the-axis-range-is-only-a-few-ulps-wide/16545 "2026-09-29T12:08:29Z")
**Posts on this page:** 2
**Page:** 1

<div class="post-metadata">

### Author: ![riep](https://discourse.vtk.org/user_avatar/discourse.vtk.org/riep/32/10758_2.png) [@riep](https://discourse.vtk.org/u/riep)
#### Post date: [September 29, 2026, 12:08pm UTC](https://discourse.vtk.org/t/bug-vtkaxis-recalculatetickspacing-loops-forever-when-the-axis-range-is-only-a-few-ulps-wide/16545/1 "2026-09-29T12:08:29Z")

</div>

Hi all,

We hit a GUI freeze (one thread at 100% CPU, never recovers) in an application that uses `vtkChartXY`. It comes from an infinite loop in `vtkAxis::RecalculateTickSpacing()`. We saw it on VTK 9.6 (Slicer’s fork), and the same code is on current `master`.

### Minimal reproducer

```python
import math, vtk

a = vtk.vtkAxis()
a.SetPosition(vtk.vtkAxis.BOTTOM)
a.SetPoint1(0, 0)
a.SetPoint2(400, 0)
a.SetRange(1e6, math.nextafter(1e6, math.inf)) # range = 1 ULP
a.RecalculateTickSpacing() # never returns
print("returned")

```

### Root cause

In `Charts/Core/vtkAxis.cxx`, `RecalculateTickSpacing()` snaps `min`/`max` onto the tick grid by stepping:

```cpp
if (this->Minimum < this->Maximum)
{
  while (min < this->Minimum)
  {
    min += this->TickInterval;
  }
  while (max > this->Maximum)
  {
    max -= this->TickInterval;
  }
}
else
{
  // same, mirrored
}

```

`TickInterval` comes from `NiceMinMax()` and is roughly `range / maxTicks`. When the range is a few ULPs, `TickInterval` is smaller than half an ULP of `min`, so `min + TickInterval == min` in double precision. The loop condition never changes and the loop never ends.

The existing guard only handles `TickInterval == 0.0`. A NaN `TickInterval` gets through too; the loops happen to exit, but NaN then reaches `GenerateTickLabels()`.

### Proposed fix

Compute the snapped bounds in closed form instead of stepping, and reject any non-positive or non-finite interval:

```cpp
      // Calculated tickinterval may be 0 (or not finite). So calculation of new
      // minimum and maximum by incrementing/decrementing using tickinterval will fail.
      if (!(this->TickInterval > 0.0) || !std::isfinite(this->TickInterval))
      {
        return;
      }
      // Snap min/max onto the first/last tick inside the axis range. This is
      // computed in closed form rather than by repeatedly adding TickInterval:
      // when the range is only a few ULPs wide (e.g. after repeated zooming),
      // TickInterval can be below the floating-point resolution of min/max and
      // the addition no longer changes the value, which would loop forever.
      const double ti = this->TickInterval;
      if (this->Minimum < this->Maximum)
      {
        if (min < this->Minimum)
        {
          min = std::max(min + std::ceil((this->Minimum - min) / ti) * ti, this->Minimum);
        }
        if (max > this->Maximum)
        {
          max = std::min(max - std::ceil((max - this->Maximum) / ti) * ti, this->Maximum);
        }
      }
      else
      {
        if (min > this->Minimum)
        {
          min = std::min(min - std::ceil((min - this->Minimum) / ti) * ti, this->Minimum);
        }
        if (max < this->Maximum)
        {
          max = std::max(max + std::ceil((this->Maximum - max) / ti) * ti, this->Maximum);
        }
      }
      this->GenerateTickLabels(min, max);

```

The result keeps the same meaning as the loops: the first tick `>= Minimum` and the last tick `<= Maximum`, mirrored for reversed axes. The `std::max`/`std::min` clamp preserves that guarantee even in the degenerate case where the step is below floating-point resolution. `<algorithm>` and `<cmath>` are already included.

I’m happy to open a merge request with this change. Is this the right approach, or would you prefer the fix in `vtkChartXY::ZoomInAxes` (e.g. refusing to zoom below a minimum relative range)? We think the fix belongs in `vtkAxis`, because anything that sets a tiny range through `SetRange()` can trigger the same hang.

Thanks!

---

<div class="post-metadata">

### Author: ![mwestphal](https://discourse.vtk.org/user_avatar/discourse.vtk.org/mwestphal/32/19_2.png) [@mwestphal](https://discourse.vtk.org/u/mwestphal)
#### Post date: [September 29, 2026, 12:10pm UTC](https://discourse.vtk.org/t/bug-vtkaxis-recalculatetickspacing-loops-forever-when-the-axis-range-is-only-a-few-ulps-wide/16545/2 "2026-09-29T12:10:25Z")

</div>

Hi @riep

A merge request with the proposed changes would be appreciated.

Best,
