FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

[MNT]: Refactor data limits · Issue #30342 · matplotlib/matplotlib · GitHub

Repository navigation

[MNT]: Refactor data limits #30342

Open
Open

Description

Summary

For now, this is a ticket to collect my thoughts. It's not completely thought though and therefore not yet actionable.

Data limits are stored in Axes.dataLim and updated via a push-mechanism (plotting functions that add artists also update the data limits). This has several disadvantages:

  • Changes to the data within an Artist cannot be automatically be picked up; one needs to explicitly call Axes.relim.
  • Syncing dataLim for shared axes is tedious
  • The concept does not work well with clearing shared axes ([MNT]: Define Axes.clear semantics #28851 (comment)).

Proposed fix

We should rearchitect data limit handling:

Break the strong coupling between Axes and data limits: Create a _DataLimits class that does all the limit related operations:

class _DataLimits:
    def __init__(axes):
          self.axes = axes
          self.datalim = BBox.null()
    
   def relim(...):
         # pull pull code from Axes.relim in here

and remove it from Axes

class AxesBase:
    def __init__(...):
        [...]
        self._datalimits = _DataLimits(self)
        # remove self.dataLim

    @property
    def dataLim(self):
        return self._datalimits.dataLim

    @dataLim.setter
    def dataLim(self, lim):
        self._datalimits.dataLim = lim

    def relim(...):
         self._datalimits.relim(....)

When we have managed this, we can improve:

  • DataLimits could handle multiple Axes. This allows much better handling of sharing We can then have one DataLimits instance, which collects data information from multiple Axes. This single DataLimits instance is used on all shared Axes. (Note: We likely have to have separate DataLimits objects for x and y to support all kinds of sharing)
  • We can make DataLimits lazy - they are just called from the Axes now. All the state handling is encapsulated in DataLimits. (Possibly, combined with a refactoring so that we can query Artists for their data limits. - Then _DataLimits just has to loop over all artists on all handled Axes and collect the limit information.

Edit: Thinking about it a bit more: data limits should not be shared - they are an individual property of the respective Axes. What needs synchronization / sharing on sharex/y is view limits.

Since Axes.dataLim is public, people can write to it (either replace the whole instance or update its attributes). Properly shielding against that is quite a hassle, because we'd need to have an immuatble BBox. So for the time being, we are bound to dataLim as BBox being the data representation. That combined with the realization that data limits should not be shared implies there's likely not a large benefit in refactoring.

Sharing should be handled on the level view limits.

We can still make the data limits lazy - as we do with viewLim https://github.com/matplotlib/matplotlib/blob/b2e8b936e057ffc7b0d1505c5703988172041d4b/lib/matplotlib/axes/_base.py#L889-893.

Activity

  1. fecrescencio22 commented on Oct 14, 2025

    Is this issue open for beginners?

  2. timhoffm commented on Oct 14, 2025

    MemberAuthor

    Thanks for the interest, but to quote myself

    For now, this is a ticket to collect my thoughts. It's not completely thought though and therefore not yet actionable.

  3. timhoffm commented on Apr 21, 2026

    MemberAuthor

    On a broader note, copying #15595 (comment):

    The following diagramm shows an outline of the limits-related call graph

    Currently the update is mostly done through the Axes methods add_[Artist] --> _update_[Artist]_limits --> update_datalim. Though there are quite a number of special cases:

    • _update_line_limits does not go through _update_datalim, but directly modifies dataLim.
    • there is no _update_* method for Collections, instead add_collection directly calls update_datalim. Additionally, that is optional because add_collection has a kwarg autolim to decide whether data limits should be updated. Further speciality: Collections have a get_datalim method whereas in the other cases, the _update_[Artist] method must exctract the limits from the corresponding Artist.
    • Images are special in that add_image does not do autoscaling, but imshow calls AxesImage.set_extent, which in turn calls update_datalim. There is also a _update_image_limits method, but this is not called when adding the image, only when doing a relim().

    So overall every Artist is a special case 😲.

    I believe we should consolidate the architecture before modifying the autoscale behavior. We can do this in multiple steps:

    1. Investigate to remove the special cases: Can update_line_limits just call update_datalim? Can add_image call _update_image_limits?
    2. I like the get_datalim method, as it's the responsibility of every Artist to know it's size in data coordinates. Every Artist that works with data should have this method, but it may return None. T.b.d. do we want to add a default impelmentation to Artist (which means also Figure grows this)? If not either we'd have to create the notion of DataArtist (either as base class or as protocol). I'm tempted to go with protocol because that is the least invasive.
    3. Remove the _update_[Artist]_limits methods. add_[Artist] and relim() should call update_datalim(artist.get_datalim()).

    To provide natural "in_autoscale" behavior, we'd need to take changes though Artist.set_in_autoscale into account. There are two ways: (1) Simple but expensive: set_in_autoscale() triggers a complete relim(). (2) Deferred limits update. Rearchitect futher so that data limits are calculated lazily. Basically autoscale would go through the artists and collect the limits.

  4. timhoffm commented on Apr 21, 2026

    MemberAuthor

    Meanwhile, Collections are more similar to other Artists: #31530 adds _update_collection_limits(), which is called by relim() and add_collection() (for the default case autolim=True; other add_[artist] functions do not have autolim and behave like autolim=True).

  5. timhoffm commented on May 15, 2026

    MemberAuthor

    Based on my refined understanding of the topic due to writing up #31673, I think we should fundamentally revisit the datalim architecture. These are currently early thoughts and many not cover all aspect yet:

    Currently, dataLim is primarily a simple data attribute.

    • It must be updated explicitly. In some cases (adding artists to plotting functions) we do this, in other cases (e.g. artist data changes, artist removal), it is left to the user to call relim().
    • AFAICS, it is not guaranteed that our incremental update_datalim() will yield the same result as a relim() call as they follow separate code paths and logic.

    I propose to migrate data limits to a smartly computed quantity: Data limits are just the bounding box of all data artists. Necessary changes:

    • An axes should know which artists include in the bounding box calculation. Effectively, the artists should just need to be registered via add_artist()
    • per-artist configurability whether an artist should count towards the data limits is a prerequisite if we take away manual control. This is the above discussed and already privately implemented set_in_autoscale() (naming t.b.d.: in_autoscale is the eventual effect, but may be a bit indirect; alternative could be something like "take into account for the data bounding box")
    • Artists need to be queryable for their data range get_datalim(). We may need some refined interface to query scale-limited data limits to replace the minpos mechanism for log scales and properly clip the <1 limit for logit. IMHO this clipping should be part of the dataLim responsibility, not the view limit calculation.
    • The Axes then just loops over all artists, checks whether they take part in_autoscale/data limits and adds up the bounding box.
    • To be checked whether we can simply loop over all Axes children or whether we need to track which one are data.
    • If needed we could add a dummy DataLimitArtist that reserves a data range without plotting anything. This should cover most cases where people had set dataLim explicitly. Maybe not fully in that it would only possibly expand the box, but worst case, people could remove all artists from autoscaling and use the DataLimitArtist to define data limits.
    • Axes.update_datalim and Axes.relim would become obsolete as there is no explicit setting of data limits
    • data limit calculation would become lazy - calculate them when needed.
    • Performance consideration: The simple implemtation would loop over the artists and query them for their limits. Assuming that we typically don't have a lot of artists, that should be fast enough. Artists could/should store their precomputed limits; e.g. Line2D calculates min/max of x/y once and updates/invalidates them when its data is manipulated. This means Artist.get_datalim() is cheap.
      I anticipate that this is good enough and we don't need to cache the datalim as a whole. Correctly caching that would add extra complexity as we must invalidate when an artist is added or removed or when data limits of an existing artist changes. While this is doable, it would require explicit API and the capability of Artists to notify its Axes about limit changes. This adds complexity and increases coupling, and should only be done if we realize that on-demand recalculation becomes too expensive.

    General note: The view limit calculation autoscale_view iterates again through all artists to handle sticky edges (which may significantly contribute to its cost). It may be reasonable to migrate this as well to data limits: When collecting limits they should separately be collected for sticky limits and non-sticky limits. This allows for efficient margin application later.

    Also note that there is a certain coupling between the Axes; example: assume you have a data point (1, -1) and y-axis is log. Then the -1 is ignored due to being out of scale. As a consequence the whole point should be ignored. It doesn't make sense to include its x-values in the x data limits as the point is not drawn anyway. Likely this should be handled by .Artist.get_datalim() querying for the valid data range; something like get_datalim(xrange=(None, None), yrange=(0, None)) and giving the Artist the responsibility to return the valid bounding box under that constraint. AFAICS after that data limits and view limits do not interact anymore across axes.

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions


      Back | FazBrowse Home | New Git URL