Skip to content

[fix] Spine.set_bounds() does not take parameter **None** as expected #30330

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: main
Choose a base branch
from

Conversation

leakyH
Copy link

@leakyH leakyH commented Jul 18, 2025

PR summary

The spines.set_bounds() can take None as input, as stated in the doc,

Passing None leaves the limit unchanged.

, but it turns out it leaves a None to the spine's low or high,because it get one None from get_bounds() by default, then the spine is invisible.

I modify the get_bounds() function's behavior, so that it can return real original bound for set_bounds() function, when self._bounds is None. Note that the get_bounds() function should NOT modify the self._bounds even if it's None.

The code I added was originally in the _adjust_location() function (and I replace the original code with get_bounds()). So the modification should be harmless.

A small demo for the problem, and you should see the bottom spine of ax1 disappear for no reason.

import matplotlib.pyplot as plt
import numpy as np

# Sample data
x = np.linspace(10, 20, 100)
y = np.sin(x)

# ------------------------
# Original (default) behavior
# ------------------------
fig, (ax1,ax2) = plt.subplots(2,1,figsize = (5,6))
ax1.plot(x, y)
ax1.set_title("Original behavior (None → axis disappears)")
ax1.spines['bottom'].set_bounds(10, None)# disappear if use original matplotlib code
ax1.spines['left'].set_bounds(-1, 1)# work
#

# ------------------------
# Expected Output
# ------------------------

# Expect to use ax.viewLim.intervalx or ax.viewLim.intervaly as default

ax2.plot(x, y)
ax2.set_title("Expected behavior (None → original view limits)")

# Here we simulate the new logic manually:

ax2.spines['bottom'].set_bounds(10, ax2.viewLim.intervalx[1])
ax2.spines['left'].set_bounds(-1, 1)

# Hide right and top spines for both to match style
for ax in [ax1, ax2]:
    ax.spines['right'].set_visible(False)
    ax.spines['top'].set_visible(False)
plt.tight_layout()
plt.savefig('fixSpineBounds.png',dpi = 300)


old_fixSpineBounds new_fixSpineBounds

The code is tested with pre-commit in a Codespace environment.

PR checklist

Copy link

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

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

Thank you for opening your first PR into Matplotlib!

If you have not heard from us in a week or so, please leave a new comment below and that should bring it to our attention. Most of our reviewers are volunteers and sometimes things fall through the cracks.

You can also join us on gitter for real-time discussion.

For details on testing, writing docs, and our review process, please see the developer guide

We strive to be a welcoming and open project. Please follow our Code of Conduct.

@rcomer
Copy link
Member

rcomer commented Jul 18, 2025

Thanks for the contribution @leakyH! I confirm I can reproduce the behaviour you report with main:

import matplotlib.pyplot as plt
import numpy as np

# Sample data
x = np.linspace(10, 20, 100)
y = np.sin(x)

fig, ax = plt.subplots()
ax.plot(x, y)
ax.spines['bottom'].set_bounds(10, None)
plt.show()
Figure_1

It looks like you are still working on your fix, so I'm going to mark this as "draft" for now. Please mark it as "ready for review" when you are ready.

@rcomer rcomer marked this pull request as draft July 18, 2025 10:02
@leakyH
Copy link
Author

leakyH commented Jul 18, 2025

Thank you @rcomer !

I just found that one of the tests failed, because I modified the self._bounds in the get_bounds() function in my original fix. Now I've solve it and I will mark it as "ready for review" when all tests pass!

@leakyH leakyH marked this pull request as ready for review July 18, 2025 16:13
@rcomer
Copy link
Member

rcomer commented Jul 18, 2025

Edited because actually this behaviour is consistent with Axes.set_xlim, as demonstrated in the next comment.

Thanks for your work on this @leakyH. Currently it is not quite right because it fixes the bound you didn't set and prevents further automatic updates to it. Adapting your example:

import matplotlib.pyplot as plt
import numpy as np

# Sample data
x = np.linspace(10, 20, 100)
y = np.sin(x)

fig, ax = plt.subplots()
ax.plot(x, y)
ax.spines['bottom'].set_bounds(10, None)
ax.plot(x*2, y)
plt.show()

with your branch, this now gives
image

But the x-spine should go from 10 to the x-axis upper limit.

I also note that Spine.get_bounds is public API and we are quite strict about making changes to the behaviour of that. It can be done, but as you see from the guidelines there are various aspects to weigh up.

I wonder if the issue might be fixed more simply by only changing the way low and high are defined in _adjust_location. Currently it uses the viewlim if neither bound was set. If one element of self._bounds is None, could we just replace that element with the relevant element of the viewlim?

@leakyH leakyH marked this pull request as draft July 18, 2025 20:08
@leakyH
Copy link
Author

leakyH commented Jul 18, 2025

yes @rcomer , thank you for your inspiring review, and I know what you mean now.

  1. In your demo, the viewLim changes after manually set_bounds(), so the spine does not align with the viewLim. You expect that when set_bounds(10, None), the None should refer to an automatic/default behaviour, which is to extend to axis's upper/lower limit, a.k.a. viewLim. I need to remind you that this is slightly different from the documentation, which says

Passing None leaves the limit unchanged.

Certainly, we can change the documentation after all. I just want to make sure you notice that and this is what we want. Since this set_bounds() API did not work in the past, maybe this behaviour change is acceptable.

As a reference, please run this code:

import matplotlib.pyplot as plt
import numpy as np

# Sample data
x = np.linspace(10, 20, 100)
y = np.sin(x)

fig, ax = plt.subplots()
ax.plot(x, y)
# ax.spines['bottom'].set_bounds(10, None)
ax.set_xlim(10, None)# Note this
ax.plot(x*2, y)
plt.savefig('modify_after_set.jpg')

You'll see the None for xlim also means to keep the original xlim, and does not change for later ax.plot.

  1. As you said, I will think of a simpler modification and try to keep get_bounds() returning the internal object self._bounds.

I think the easiest way is to modify the _adjust_location, and allow the spine to automatically extend to axis's upper/lower limit. But it would be better to clarify what we want before further modifications to the code.

Thank you!

@rcomer
Copy link
Member

rcomer commented Jul 19, 2025

Thank you for thinking this through! You are right and I actually had not realised that set_xlim works like that. I agree we should make Spine.set_bounds be consistent with set_xlim, so the behaviour you currently have is correct.

@leakyH
Copy link
Author

leakyH commented Jul 19, 2025

Hi @rcomer , I've updated the code:

  1. get_bounds() now returns the internal object self._bounds as it did.
  2. In set_bounds(), it gets the original bounds as _adjust_location did, so the logic is extracted as a function called _get_bounds_or_viewLim.

And to whom may concern, this PR initailly processed the self._bounds is None situation in the get_bounds() public API, which may bring extra complexity. Based on the feedback, now the set_bounds and _adjust_location both use the internal function _get_bounds_or_viewLim.

@leakyH leakyH marked this pull request as ready for review July 19, 2025 20:07
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