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

Cartopy axes_grid_basic example broken by Matplotlib 3.6 · Issue #24053 · matplotlib/matplotlib · GitHub

Repository navigation

Cartopy axes_grid_basic example broken by Matplotlib 3.6 #24053

Closed

Description

Cartopy's axes_grid_basic was broken by Matplotlib 3.6. The relevant call is:

    axgr = AxesGrid(fig, 111, axes_class=axes_class,
                    nrows_ncols=(3, 2),
                    axes_pad=0.6,
                    cbar_location='right',
                    cbar_mode='single',
                    cbar_pad=0.2,
                    cbar_size='3%',
                    label_mode='')  # note the empty label_mode

In #23550, we began explicitly checking for the documented values of label_mode, ('All', 'L', '1'), which this runs afoul of. It appears the previous code let '' (or any option not handled) essentially work as a noop for set_label_mode().

Based on the output when the example worked, '1' would be fine behavior-wise, but that currently produces in this example:

/Users/rmay/repos/cartopy/examples/miscellanea/axes_grid_basic.py:48: MatplotlibDeprecationWarning: The resize_event function was deprecated in Matplotlib 3.6 and will be removed two minor releases later. Use callbacks.process('resize_event', ResizeEvent(...)) instead.
  fig = plt.figure()
Traceback (most recent call last):
  File "/Users/rmay/repos/cartopy/examples/miscellanea/axes_grid_basic.py", line 78, in <module>
    main()
  File "/Users/rmay/repos/cartopy/examples/miscellanea/axes_grid_basic.py", line 49, in main
    axgr = AxesGrid(fig, 111,
  File "/Users/rmay/miniconda3/envs/py310/lib/python3.10/site-packages/mpl_toolkits/axes_grid1/axes_grid.py", line 391, in __init__
    super().__init__(
  File "/Users/rmay/miniconda3/envs/py310/lib/python3.10/site-packages/mpl_toolkits/axes_grid1/axes_grid.py", line 174, in __init__
    self.set_label_mode(label_mode)
  File "/Users/rmay/miniconda3/envs/py310/lib/python3.10/site-packages/mpl_toolkits/axes_grid1/axes_grid.py", line 295, in set_label_mode
    _tick_only(ax, bottom_on=True, left_on=True)
  File "/Users/rmay/miniconda3/envs/py310/lib/python3.10/site-packages/mpl_toolkits/axes_grid1/axes_grid.py", line 16, in _tick_only
    ax.axis["bottom"].toggle(ticklabels=bottom_off, label=bottom_off)
TypeError: 'method' object is not subscriptable

This is because, while AxesGrid supposedly supports arbitrary classes by passing axes_class, there's a huge caveat: the default is mpl_toolkits.axes_grid1.mpl_axes.Axes (TERRIBLY CONFUSING NAME--it's imported as just Axes in axis_grid.py). This is a custom subclass that adds:

    @property
    def axis(self):
        return self._axislines

    def clear(self):
        # docstring inherited
        super().clear()
        # Init axis artists.
        self._axislines = self.AxisDict(self)
        self._axislines.update(
            bottom=SimpleAxisArtist(self.xaxis, 1, self.spines["bottom"]),
            top=SimpleAxisArtist(self.xaxis, 2, self.spines["top"]),
            left=SimpleAxisArtist(self.yaxis, 1, self.spines["left"]),
            right=SimpleAxisArtist(self.yaxis, 2, self.spines["right"]))

Options I can think of:

  1. Restore a noop option for set_label_mode
  2. Fix AxesGrid to work more generally with axes_class--monkey-patch/subclass to add the necessary modifications
  3. Document that axes_class needs to have certain behavior
  4. Fix AxesGrid to not rely on a property that OVERRIDES matplotlib.axes.Axes.axis with a different type that also manually dispatches to the unbound axis() method 😱
    class AxisDict(dict):
        def __init__(self, axes):
            self.axes = axes
            super().__init__()

        def __getitem__(self, k):
            if isinstance(k, tuple):
                r = SimpleChainedObjects(
                    # super() within a list comprehension needs explicit args.
                    [super(Axes.AxisDict, self).__getitem__(k1) for k1 in k])
                return r
            elif isinstance(k, slice):
                if k.start is None and k.stop is None and k.step is None:
                    return SimpleChainedObjects(list(self.values()))
                else:
                    raise ValueError("Unsupported slice")
            else:
                return dict.__getitem__(self, k)

        def __call__(self, *v, **kwargs):
            return maxes.Axes.axis(self.axes, *v, **kwargs)

Anyone have any opinions?

Activity

  1. added this to the v3.6.1 milestone on Sep 29, 2022
  2. oscargus commented on Sep 29, 2022

    Member

    A quick thought (since I broke it): there should be a way to not get any labels. Maybe 'none'?

    (If I recall correctly all those three do generate labels.)

    Then, maybe we also want to do something more..

  3. dopplershift commented on Sep 29, 2022

    ContributorAuthor

    A quick thought (since I broke it): there should be a way to not get any labels. Maybe 'none'?

    Just wanted to clarify that the previous (accidentally) "working" behavior (at least for Cartopy) wasn't no labels:

    EDIT: I think it was more "don't touch my labels".

  4. jklymak commented on Sep 29, 2022

    Member

    Super off topic, but does this example work with a normal grid of subplots with layout='compress'?

  5. oscargus commented on Sep 30, 2022

    Member

    Just wanted to clarify that the previous (accidentally) "working" behavior (at least for Cartopy) wasn't no labels:

    Ahh, sorry about that. Was reading on the phone in bed and wanted to confirm that I broke something...

    Looking at the code, it is not really obvious what happens with the "all" case, which somehow is documented to do what is the expected output(?), and the difference to not doing anything.

    I'll try to look more into it over the weekend. I guess either passing "all" may work and one should deprecate passing something else or we should add a fourth option corresponding to passing something else (and deprecate passing anything other than any of the four options). Not sure if we should revert for 3.6 or if it is OK to deprecate as part of 3.6.1.

  6. rcomer commented on Oct 1, 2022

    Member

    Super off topic, but does this example work with a normal grid of subplots with layout='compress'?

    I can't believe how easy this was 🤩
    import cartopy.crs as ccrs
    from cartopy.mpl.ticker import LongitudeFormatter, LatitudeFormatter
    import matplotlib.pyplot as plt
    import numpy as np
    
    
    def sample_data_3d(shape):
        """Return `lons`, `lats`, `times` and fake `data`"""
        ntimes, nlats, nlons = shape
        lats = np.linspace(-np.pi / 2, np.pi / 2, nlats)
        lons = np.linspace(0, 2 * np.pi, nlons)
        lons, lats = np.meshgrid(lons, lats)
        wave = 0.75 * (np.sin(2 * lats) ** 8) * np.cos(4 * lons)
        mean = 0.5 * np.cos(2 * lats) * ((np.sin(2 * lats)) ** 2 + 2)
    
        lats = np.rad2deg(lats)
        lons = np.rad2deg(lons)
        data = wave + mean
    
        times = np.linspace(-1, 1, ntimes)
        new_shape = data.shape + (ntimes, )
        data = np.rollaxis(data.repeat(ntimes).reshape(new_shape), -1)
        data *= times[:, np.newaxis, np.newaxis]
    
        return lons, lats, times, data
    
    
    def main():
        projection = ccrs.PlateCarree()
    
        lons, lats, times, data = sample_data_3d((6, 73, 145))
        
        for figsize in [(6, 6), (8, 4)]:
    
            fig, ax_arr = plt.subplots(3, 2, layout="compressed",
                                       subplot_kw=dict(projection=projection),
                                       figsize=figsize)
    
            for i, ax in enumerate(ax_arr.flat):
                ax.coastlines()
                ax.set_xticks(np.linspace(-180, 180, 5), crs=projection)
                ax.set_yticks(np.linspace(-90, 90, 5), crs=projection)
                lon_formatter = LongitudeFormatter(zero_direction_label=True)
                lat_formatter = LatitudeFormatter()
                ax.xaxis.set_major_formatter(lon_formatter)
                ax.yaxis.set_major_formatter(lat_formatter)
    
                p = ax.contourf(lons, lats, data[i, ...],
                                transform=projection,
                                cmap='RdBu')
    
            fig.colorbar(p, ax=ax_arr[:, -1])
    
            plt.show()
    
    
    if __name__ == '__main__':
        main()


  7. oscargus commented on Oct 2, 2022

    Member

    Just to confirm @dopplershift : will you get the same/correct behavior if you pass "all"? I cannot see any difference in my tests...

    If so, I think a deprecation warning of passing an undefined label_mode is the (first) way to go.

    If not, we should come up with a good name for that label_mode.

    (It may be that toggle which is called later is not properly working though, #23595. But 'all' will turn on the labels, which seems to be the wanted thing here.)

  8. oscargus commented on Oct 2, 2022

    Member

    I tried to run the Cartopy example from the git version and failed, so I cannot confirm that it is working. Just in case, I added a fourth option, "keep" for this case.

  9. dopplershift commented on Oct 2, 2022

    ContributorAuthor

    @oscargus No, it doesn't work with 'all' for the same reasons as I originally enumerated above: AxesGrid, while it advertises support for using a custom class, only works with classes that do that terrible process of overriding the .axis attribute with a custom dictionary. IMO, while restoring the previous option is a nice work-around, AxesGrid needs a refactor to do less silly things--but I'm waiting to hear everyone else weigh in.

  10. jklymak commented on Oct 2, 2022

    Member

    Layout='compress' is an AxesGrid replacement

    If someone refactors AxesGrid I'd suggest they do so on the main library and allow a clean API break with the toolkit version.

  11. dopplershift commented on Oct 3, 2022

    ContributorAuthor

    If layout='compress' really is an AxesGrid replacement, I'm happy to update the Cartopy example to use it eventually. Seeing as it was only added in 3.6, though, having 3.6.1 unbreak the example (#24084) will be good.

    That certainly eliminates any interest I have in refactoring AxesGrid.

  12. tacaswell commented on Oct 3, 2022

    Member

    @dopplershift See @rcomer 's comment above.

  13. jklymak commented on Oct 3, 2022

    Member

    On topic: agree with unbreaking 3.6.1.

    Off topic: I think layout='compress' is a good replacement for AxesGrid, however it is new, and has the frailty of a layout engine that solves a set of constrains. Conversely, AxesGrid is completely deterministic, and hence less fragile. So I could easily be convinced that an AxesGrid artist would be a good feature for the main library with the right API and design choices.

  14. dopplershift commented on Oct 3, 2022

    ContributorAuthor

    @tacaswell Yeah, saw that. Looks good. Trying to figure out the right balance in moving a fringe example to a new feature that just got released 2 weeks ago.

  15. rcomer commented on Oct 3, 2022

    Member

    If it helps, I also tried the example with layout='constrained' and the default figsize. That also looked good, because it just happens that those maps are the right shape to fit 6 of them well.

    But even if the Cartopy example changes to a newer approach, it’s still good to unbreak 3.6.1 for any Cartopy users who adapted the example into their code.

  16. QuLogic commented on Oct 6, 2022

    Member

    IIUC, this is fixed by #24084

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

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions


      Back | FazBrowse Home | New Git URL