Thanks to visit codestin.com
Credit goes to github.com

Skip to content

BUG: Fix relim() to support Collection artists (scatter, etc.)#31530

Merged
timhoffm merged 9 commits intomatplotlib:mainfrom
tinezivic:fix-relim-scatter-offsets
Apr 21, 2026
Merged

BUG: Fix relim() to support Collection artists (scatter, etc.)#31530
timhoffm merged 9 commits intomatplotlib:mainfrom
tinezivic:fix-relim-scatter-offsets

Conversation

@tinezivic
Copy link
Copy Markdown
Contributor

@tinezivic tinezivic commented Apr 19, 2026

PR summary

relim() completely ignored Collection artists (scatter plots, PathCollection, etc.) — calling ax.relim() after scatter.set_offsets() had no effect on the axis limits. Closes #30859.

Why is this change necessary?
Users who update scatter data dynamically (e.g. animation loops, interactive plots) rely on relim() + autoscale_view() to recalculate axis limits. For line artists this works; for scatter/Collection artists it silently did nothing.

What problem does it solve?
After scatter.set_offsets(new_data), calling ax.relim(); ax.autoscale_view() now correctly updates the axis limits to fit the new data.

Root cause — two bugs, one fix:

  1. add_collection() never called collection._set_in_autoscale(), unlike add_line(), add_patch(), and add_image(). As a result relim() skipped all Collections because _get_in_autoscale() returned False (the default). The fix calls _set_in_autoscale(bool(autolim)) so that autolim=False correctly opts the collection out of future relim() calls.

  2. relim() in _base.py had no elif isinstance(a, mcoll.Collection) branch. Even with flag fixed, the data limits would never have been picked up. The fix adds that branch, calling a new _update_collection_limits() helper that mirrors the logic already used in add_collection().

Minimal reproduction:

import matplotlib.pyplot as plt
import numpy as np

fig, ax = plt.subplots()
scatter = ax.scatter([], [])
xs = np.linspace(0, 10, 100)
scatter.set_offsets(np.column_stack((xs, np.sin(xs))))

ax.relim()
ax.autoscale_view()
print(ax.get_xlim())  # Before fix: (0.0, 1.0)  After fix: (-0.5, 10.5)
plt.show()

Before fix — axis stays at default [0, 1], data is cut off:

before fix

After fix — axis correctly scales to data extent:

after fix

AI Disclosure

I used GitHub Copilot (Claude Sonnet 4.6) to assist with code generation, diff review, and drafting documentation (including doc/api/next_api_changes/behavior/31530-TZ.rst). The root cause analysis, understanding of the _in_autoscale mechanism, identification of both bug sites, and verification of the fix logic were done by me.

PR checklist

  • "closes [Bug]: ax.relim() ignores scatter artist #30859" is in the body of the PR description
  • new and changed code is tested — three regression tests in lib/matplotlib/tests/test_axes.py:
    • test_relim_collection: initial scatter limits, limits after set_offsets(), visible_only=True
    • test_relim_collection_autolim_false: collection added with autolim=False must not affect limits after relim()
    • test_relim_collection_log_scale: relim() on log-scaled axes exercises the minpos path correctly
  • Plotting related features are demonstrated in an example — N/A (bugfix, not a new feature)
  • New Features and API Changes are noted with a directive and release note — added doc/api/next_api_changes/behavior/31530-TZ.rst
  • Documentation complies with general and docstring guidelines — no public API changed

@tinezivic tinezivic force-pushed the fix-relim-scatter-offsets branch from 1d1d33f to cb8bce5 Compare April 19, 2026 11:25
…) previously skipped Collection instances entirely. Two issues: 1. add_collection() did not call collection._set_in_autoscale(True), unlike add_line(), add_patch(), and add_image(). This caused relim() to skip collections via the _get_in_autoscale() check. 2. relim() had no handling for Collection instances. Added an elif branch that calls get_datalim() and update_datalim(), mirroring the logic already used in add_collection(). Closes matplotlib#30859
@tinezivic tinezivic force-pushed the fix-relim-scatter-offsets branch from cb8bce5 to 07127b6 Compare April 19, 2026 12:58
@rcomer
Copy link
Copy Markdown
Member

rcomer commented Apr 19, 2026

Please post a screenshot showing the result of the script from the issue. Please also fill out the PR summary using our template.

@rcomer rcomer added the status: autoclose candidate PRs that are not yet ready for review and may be automatically closed in two weeks label Apr 19, 2026
@github-actions
Copy link
Copy Markdown

⏰ This pull request might be automatically closed in two weeks from now.

Thank you for your contribution to Matplotlib and for the effort you have put into this PR. This pull request does not yet meet the quality and clarity standards needed for an effective review. Project maintainers have limited time for code reviews, and our goal is to prioritize well-prepared contributions to keep Matplotlib maintainable.

Matplotlib maintainers cannot provide one-to-one guidance on this PR. However, if you ask focused, well-researched questions, a community member may be willing to help. 💬

To increase the chance of a productive review:

As the author, you are responsible for driving this PR, which entails doing necessary background research as well as presenting its context and your thought process. If you are a new contributor, or do not know how to fulfill these requirements, we recommend that you familiarize yourself with Matplotlib's development conventions or engage with the community via our Discourse or one of our meetings before submitting code.

If you substantially improve this PR within two weeks, leave a comment and a team member may remove the status: autoclose candidate label and the PR stays open. Cosmetic changes or incomplete fixes will not be sufficient. Maintainers will assess improvements on their own schedule. Please do not ping (@) maintainers.

Mirror the comments from add_collection() that explain why minpos
is included (log scale support) and why contains_branch_separately
is used to conditionally update x/y limits.
@tinezivic
Copy link
Copy Markdown
Contributor Author

Hi, just wanted to flag that this PR has been significantly updated since it was first opened. The description now includes a full root-cause explanation, a minimal reproducer with before/after images, a dedicated test (test_relim_collection), and a behavior change note in doc/api/next_api_changes/. Happy to answer any questions or make adjustments based on feedback — thanks for taking a look!

_set_in_autoscale(True) was set unconditionally in add_collection(),
outside the 'if autolim:' block. This meant that any collection added
with autolim=False would still be picked up by relim() later.

Fix: move _set_in_autoscale(True) inside the 'if autolim:' block so
that relim() only considers collections that explicitly opted in.

Add two regression tests:
- test_relim_collection_autolim_false: verifies that a collection added
  with autolim=False does not affect limits after relim().
- test_relim_collection_log_scale: verifies that relim() + autoscale_view()
  works correctly for a Collection on log-scaled axes (exercises the
  minpos path).

Closes matplotlib#30859
@tinezivic
Copy link
Copy Markdown
Contributor Author

Pushed one more focused update to address a semantic issue in the fix.

_set_in_autoscale(True) was being called unconditionally in add_collection(), outside the if autolim: block — meaning collections added with autolim=False would silently participate in relim() anyway, breaking the opt-out contract.

Changes in this update:

  • moved _set_in_autoscale(True) inside if autolim: to preserve autolim=False semantics
  • added test_relim_collection_autolim_false — verifies collections with autolim=False do not affect limits after relim()
  • added test_relim_collection_log_scale — verifies relim() works correctly on log-scaled axes (exercises the minpos path)

The goal remains a narrow bugfix for #30859, not a redesign of autoscale semantics.

@tinezivic tinezivic force-pushed the fix-relim-scatter-offsets branch from 53f5507 to 3da28af Compare April 20, 2026 03:55
@rcomer rcomer removed the status: autoclose candidate PRs that are not yet ready for review and may be automatically closed in two weeks label Apr 20, 2026
Comment thread lib/matplotlib/axes/_base.py Outdated
elif isinstance(artist, mimage.AxesImage):
self._update_image_limits(artist)
elif isinstance(artist, mcoll.Collection):
datalim = artist.get_datalim(self.transData)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This is on the wrong abstraction level and duplicates code from add_collection

If you want a narrow fix, please add _update_collection_limits and call it here - even if #15595 (comment) suggests that the update functions should eventually be removed.

Copy link
Copy Markdown
Contributor Author

@tinezivic tinezivic Apr 20, 2026

Choose a reason for hiding this comment

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

Done — extracted _update_collection_limits() following the same pattern as _update_line_limits, _update_patch_limits, and _update_image_limits. Both add_collection() and relim() now call the helper instead of repeating the get_datalim / minpos-concatenation / contains_branch_separately / update_datalim block inline. See commit 68a6269.

…on and relim()

Refactor per reviewer feedback (timhoffm): the Collection data-limit logic
in relim() was duplicating code from add_collection(). Extract it into a new
private method _update_collection_limits(), following the existing pattern of
_update_line_limits(), _update_patch_limits(), and _update_image_limits().

Both add_collection() and relim() now call the helper instead of repeating
the get_datalim / minpos-concatenation / contains_branch_separately /
update_datalim block inline.
Copy link
Copy Markdown
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.

On a closer look, I'm unclear whether it is reasonable to have the "small" fix of relim() using

Comment thread lib/matplotlib/tests/test_axes.py Outdated
Comment on lines +6518 to +6522
ax.autoscale_view()
xlim = ax.get_xlim()
ylim = ax.get_ylim()
assert xlim[0] <= 1 and xlim[1] >= 3
assert ylim[0] <= 4 and ylim[1] >= 6
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

relim works on ax.dataLim. Please test its exact values and not the view limits.

Comment thread lib/matplotlib/axes/_base.py Outdated
Comment on lines 2391 to 2392
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
Since 3.11 `autolim=True` matches the standard behavior
of other ``add_[artist]`` methods: Axes data and view limits
are both updated in the method, and the collection will
be considered in future data limit updates through
`.relim`.
Prior to matplotlib 3.11 this was only a one-time update
of the data limits. Updating view limits required an
explicit calls to `~.Axes.autoscale_view`, and collections
did not take part in `.relim`.

Comment thread lib/matplotlib/axes/_base.py Outdated
# Mark collection as participating in relim() only when autolim
# is enabled. If autolim=False the caller explicitly opted out,
# so relim() must not pick this collection up later.
collection._set_in_autoscale(True)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Should we revert the logic here? Historically, Collections did not take part in autoscaling. When introducing the in_autoscale flag, we therefore defaulted it to False on Collections, but True for all other Artists.
For better consistency we may now want Collections defaulting to True as well and explicitly set to False in case of autolim=False as special-cased backward-compatibility. Am I missing a point why we can't do that?

Comment thread lib/matplotlib/axes/_base.py
@tinezivic
Copy link
Copy Markdown
Contributor Author

Addressed the latest review concerns with a narrow-scope update focused on #30859:

  • Switched regression tests to assert Axes.dataLim (and minpos) directly instead of relying on view limits.
  • Made add_collection(..., autolim=...) explicitly control relim participation via _set_in_autoscale(bool(autolim)), so autolim=False remains a strict opt-out.
  • Updated the add_collection docstring accordingly and removed the stale Artist.remove note claiming collections are invisible to relim().

I intentionally kept this PR narrow (bugfix + behavior/docs/tests alignment) rather than taking the broader autoscale architecture route, which seems better handled in follow-up design work.

Line data [0, 1] x [0, 1] gives minpos=[1., 1.] (minimum *positive*
value), not [inf, inf].  The [inf, inf] sentinel only appears when
there are no positive values at all.
Comment thread lib/matplotlib/axes/_base.py Outdated
Comment on lines +2412 to +2414
# Keep relim() participation aligned with the autolim argument.
# autolim can also be the internal sentinel "_datalim_only".
collection._set_in_autoscale(bool(autolim))
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Sorry, my bad I had a wrong recollection of Artist._in_autoscale being True by default. It's actually False, and set to True in the add_[artist] methods. So this should move back into if autolim in form of a simple _set_in_autoscale(True)

Comment thread lib/matplotlib/axes/_base.py Outdated
The collection to add.
autolim : bool
Whether to update data and view limits.
Whether to update data limits and request autoscaling.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The original wording was clearer to the user. "request autoscaling" is an internal concept for lazy evaluation of autoscaled view limits. The user should not see a difference between "request autoscaling" and an immediate update of the view limits. Please revert.

Comment thread lib/matplotlib/axes/_base.py Outdated
Comment on lines +2389 to +2390
If *False*, the collection is explicitly excluded from
`~.Axes.relim`.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
If *False*, the collection is explicitly excluded from
`~.Axes.relim`.
If *False*, the collection does not take part in any limit
operations.

…toscale semantics

- Revert autolim docstring first line to original user-facing wording
  ("update data and view limits" instead of internal "request autoscaling")
- Adopt reviewer suggestion for If-False clause: "does not take part in
  any limit operations"
- Expand versionchanged:: 3.11 note to explain both pre-3.11 behavior
  and the new relim participation (per reviewer suggestion)
- Move _set_in_autoscale(True) inside if autolim: block (simpler and
  consistent with how add_line/add_patch/add_image do it)
- Update stale TODO in Artist.remove: the "collections relim problem"
  referenced has been fixed; rephrase to reflect the remaining
  long-term architectural goal
@tinezivic
Copy link
Copy Markdown
Contributor Author

Addressed the latest reviewer comments in commit a1c58ec:

  • autolim docstring, first line — reverted to original user-facing wording: "Whether to update data and view limits."
  • If False clause — adopted the suggested wording: "the collection does not take part in any limit operations."
  • versionchanged:: 3.11 — expanded with the full explanation from the reviewer suggestion: explains both the new behavior (relim participation) and what changed relative to pre-3.11
  • _set_in_autoscale(True) — moved back inside if autolim: as a simple True assignment, consistent with add_line, add_patch, and add_image
  • TODO in Artist.remove — updated: removed the phrase "fix for the collections relim problem" (the problem is now fixed); the remaining TODO reflects the long-term architectural goal of moving limit logic into the artist itself

All three regression tests pass locally (test_relim_collection, test_relim_collection_autolim_false, test_relim_collection_log_scale).

_set_in_autoscale(True) must not be called for collections added with
autolim='_datalim_only' — that sentinel means 'update datalim once but
do not enter the autoscale/relim system'. Previously, because the call
was outside the if autolim: block, bool('_datalim_only') == True meant
3D collections inadvertently got _in_autoscale=True. Fixed by gating
the call on autolim != '_datalim_only', consistent with the existing
_request_autoscale_view guard directly below.

Also add test_relim_collection_autoscale_view: an end-to-end regression
that checks ax.get_xlim()/get_ylim() after relim()+autoscale_view(),
matching the exact user-facing scenario from GH#30859.
@tinezivic
Copy link
Copy Markdown
Contributor Author

One more focused update (f90c147) fixing a subtle semantic issue I caught during review:

_datalim_only semantics fix_set_in_autoscale(True) was being called for collections added with autolim="_datalim_only" (the internal 3D sentinel), because the truthy string would pass the if autolim: check. That sentinel means "update datalim once, do not enter the autoscale/relim system", so these collections should keep _in_autoscale=False. Fixed by gating the call on autolim != "_datalim_only", consistent with the existing _request_autoscale_view guard directly below it. 141 non-image 3D tests pass.

New end-to-end test — added test_relim_collection_autoscale_view: reproduces the exact user-facing scenario from #30859 by checking ax.get_xlim()/get_ylim() after relim() + autoscale_view(), not just dataLim.

All 5 relim tests pass locally.

Comment thread lib/matplotlib/axes/_base.py Outdated
Comment thread lib/matplotlib/axes/_base.py Outdated
Comment thread lib/matplotlib/artist.py Outdated
- Fix backticks on autolim=True in add_collection docstring
  (single backtick was interpreted as cross-reference by Sphinx)
- 'explicit calls' -> 'explicit call' per reviewer suggestion
- Remove out-of-place TODO comments from artist.py remove()
@timhoffm timhoffm added this to the v3.11.0 milestone Apr 21, 2026
@timhoffm timhoffm merged commit 1068992 into matplotlib:main Apr 21, 2026
37 of 41 checks passed
@timhoffm
Copy link
Copy Markdown
Member

Thanks @tinezivic, and congratulations on your first contribution to matplotlib! 🎉 We hope to see you back.

@tinezivic
Copy link
Copy Markdown
Contributor Author

Thank you @timhoffm and @jklymak for the thorough review and the patience with the iterations — learned a lot from the process. Looking forward to contributing more!

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]: ax.relim() ignores scatter artist

4 participants