Skip to content

BUG: Fix IndexLocator.tick_values returning values greater than vmax#31091

Merged
timhoffm merged 5 commits intomatplotlib:mainfrom
saakshigupta2002:fix-indexlocator-tick-values-vmax
Feb 6, 2026
Merged

BUG: Fix IndexLocator.tick_values returning values greater than vmax#31091
timhoffm merged 5 commits intomatplotlib:mainfrom
saakshigupta2002:fix-indexlocator-tick-values-vmax

Conversation

@saakshigupta2002
Copy link
Contributor

@saakshigupta2002 saakshigupta2002 commented Feb 5, 2026

Pull Request: Fix IndexLocator.tick_values returning values greater than vmax

Summary

This PR fixes a bug in IndexLocator.tick_values() where tick values could exceed the specified vmax parameter. This issue manifested most notably when using colorbars with discrete colormaps and NoNorm normalization, where cbar.get_ticks() would return an incorrect array containing values outside the valid range.

The Problem

When creating a colorbar using a ScalarMappable with NoNorm normalization and a discrete colormap (e.g., viridis with a fixed number of colors), the ticks returned by colorbar.get_ticks() did not align with the visually displayed tick positions. Specifically, calling cbar.set_ticks(cbar.get_ticks()) would unexpectedly change the ticks because get_ticks() returned values outside the valid range.

Example reproduction:

import matplotlib.pyplot as plt
from matplotlib import cm, colors

data = [1, 2, 3, 4, 5]
fig, ax = plt.subplots()
cbar = fig.colorbar(
    cm.ScalarMappable(norm=colors.NoNorm(), cmap=plt.get_cmap("viridis", len(data))),
    ax=ax
)
print(cbar.get_ticks())
# Actual (before fix): [0. 1. 2. 3. 4. 5.]
# Expected (after fix): [0. 1. 2. 3. 4.]

Root Cause

The issue was in IndexLocator.tick_values() in lib/matplotlib/ticker.py:

def tick_values(self, vmin, vmax):
    return self.raise_if_exceeds(
        np.arange(vmin + self.offset, vmax + 1, self._base))

The vmax + 1 in the np.arange call was intended to make the interval inclusive of vmax (since np.arange produces a semi-open interval [start, stop)). However, this overshot the target — combined with certain offset values, it could generate tick positions that exceed vmax. For instance, with offset=0.5 and vmax=4, the array [0.5, 1.5, 2.5, 3.5, 4.5] would be generated, where 4.5 > vmax.

The Fix

Replaced vmax + 1 with vmax + 1e-12 as the stop value for np.arange. This provides a minimal epsilon beyond vmax that ensures vmax itself is included in the closed interval [vmin, vmax], without overshooting to produce values beyond vmax:

def tick_values(self, vmin, vmax):
    # We want tick values in the closed interval [vmin, vmax].
    # Since np.arange(start, stop) returns values in the semi-open interval
    # [start, stop), we add a minimal offset so that stop = vmax + eps
    tick_values = np.arange(vmin + self.offset, vmax + 1e-12, self._base)
    return self.raise_if_exceeds(tick_values)

This directly generates the correct number of ticks without any post-filtering, which is both cleaner and more efficient.


Changes

Modified Files

  1. lib/matplotlib/ticker.py

    • Modified IndexLocator.tick_values() to use vmax + 1e-12 instead of vmax + 1 as the np.arange stop value, ensuring tick values stay within the [vmin, vmax] closed interval
  2. lib/matplotlib/tests/test_ticker.py

    • Added test_tick_values_not_exceeding_vmax() test method to TestIndexLocator class
    • Tests verify correct tick generation for:
      • Integer offset (offset=0)
      • Fractional offset (offset=0.5)
      • Step size greater than 1 (base=2)

Test Plan

  • Added unit tests for the fix in test_ticker.py
  • Run the full test suite: pytest lib/matplotlib/tests/test_ticker.py -v
  • Verify the reproduction case from the issue is fixed
  • Check that existing colorbar functionality is not affected

New Test Cases

def test_tick_values_not_exceeding_vmax(self):
    """
    Test that tick_values does not return values greater than vmax.
    """
    # Test case where offset=0 could cause vmax to be included incorrectly
    index = mticker.IndexLocator(base=1, offset=0)
    assert_array_equal(index.tick_values(0, 4), [0, 1, 2, 3, 4])

    # Test case with fractional offset
    index = mticker.IndexLocator(base=1, offset=0.5)
    assert_array_equal(index.tick_values(0, 4), [0.5, 1.5, 2.5, 3.5])

    # Test case with base > 1
    index = mticker.IndexLocator(base=2, offset=0)
    assert_array_equal(index.tick_values(0, 5), [0, 2, 4])

Related Issues


Checklist

  • Code follows the project's style guidelines
  • Changes are minimal and focused on the specific bug
  • Added regression test to prevent future occurrences
  • Commit message follows conventional format
  • CI tests pass

Fix IndexLocator.tick_values() to not return tick values that exceed
vmax. Previously, when using IndexLocator with certain offset values,
the generated ticks could exceed the specified vmax, causing issues
like colorbar.get_ticks() returning incorrect values for discrete
colormaps with NoNorm normalization.

The fix adds a filter to remove any tick values greater than vmax
after generating the initial tick positions.

Fixes matplotlib#31086
Copy link
Contributor

@scottshambaugh scottshambaugh left a comment

Choose a reason for hiding this comment

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

Looks good to me, thanks!

Copy link
Member

@timhoffm timhoffm left a comment

Choose a reason for hiding this comment

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

Thanks for the contribution.

saakshigupta2002 and others added 3 commits February 6, 2026 09:49
Co-authored-by: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com>
Co-authored-by: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com>
Co-authored-by: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com>
@saakshigupta2002
Copy link
Contributor Author

Thanks for the contribution.

Makes sense. Thanks for the suggestions! committed them :)

Use assert_array_equal directly for the fractional offset and
base > 1 test cases, matching the style of the first test case.
@timhoffm timhoffm merged commit 035e207 into matplotlib:main Feb 6, 2026
33 of 37 checks passed
@QuLogic QuLogic added this to the v3.11.0 milestone Feb 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Colorbar get_ticks() return the incorrect array

4 participants