cartopy icon indicating copy to clipboard operation
cartopy copied to clipboard

Shared axes broken on pan

Open dopplershift opened this issue 4 years ago • 8 comments

Description

It looks like when sharing axes, and you pan one of them interactively, the other(s) move around. This is a consequence of our new (0.18) background patch and spine, which need to be updated with the data limits. When axes are shared, the original axes gets notified of data limits properly and re-clips as appropriate. Unfortunately, this not happening for any axes sharing the original--this is because when matplotlib syncs limit changes to shared axes, it explicitly says not to emit any events for those axes. 😞

Code to reproduce

import matplotlib.pyplot as plt
import cartopy.crs as ccrs
import cartopy.feature as cfeature

fig = plt.figure(figsize=[10, 5])
ax1 = fig.add_subplot(1, 2, 1, projection=ccrs.SouthPolarStereo())
ax2 = fig.add_subplot(1, 2, 2, projection=ccrs.SouthPolarStereo(), sharex=ax1)

# Limit the map to -60 degrees latitude and below.
ax1.set_extent([-180, 180, -90, -60], ccrs.PlateCarree())
ax2.set_extent([-180, 180, -90, -60], ccrs.PlateCarree())

ax1.add_feature(cfeature.LAND)
ax2.add_feature(cfeature.LAND)

plt.pause(0.01)

ax1.set_xlim(-6180148.99567504, 619506.5641910564)
plt.draw()

plt.show()

Gives:

image

Originally mentioned in #1620. The reason I'm opening the issue is because I'm really not sure what the cleanest way to fix this is:

  1. Notify shared axes in our existing event callback. This requires using matplotlib's private _shared_x_axes and _shared_y_axes. Not wild about using private attributes, especially ones that have been refactored in the past.

  2. Override set_xlim and set_ylim on GeoAxes. Can just set the stale flags when the limits are set. Might not even need the event callback any more. Downside is needing to match matplotlib's extensive signature and docstring just so we can hook in. (If only there was some kind of event dispatch we could use...) Also need to deal with the fact that .patch is not available when set_[y|x]lim is first called, so feels...unclean.

  3. ~Hook in somehow when axes are first shared and register event handler on limit changes on the other axes~ I didn't even try this one, but it seems terrible.

  4. ?

I can confirm either (1) or (2) do solve the issue. Just not sure what's the least annoying. Thoughts @QuLogic ? Any other ideas?

dopplershift avatar Aug 22 '20 06:08 dopplershift

Well, looks like get_shared_x_axes(), etc. are functions on the latest matplotlib, not sure how far back they go...

dopplershift avatar Aug 24 '20 17:08 dopplershift

It looks like this is the only issue/PR open on the 0.18.x milestone.

get_shared_x_axes() goes back quite far, at least 1.4.

Do you want to open a PR with that fix?

greglucas avatar Oct 08 '20 01:10 greglucas

Sure, but it will be a week or two before I can get there.

dopplershift avatar Oct 13 '20 05:10 dopplershift

any update on this? dynamically zooming shared axes seems to be still broken in cartopy 0.19.0

raphaelquast avatar May 31 '21 14:05 raphaelquast

Any updates? It still exists in v0.20.1.

zxdawn avatar Nov 12 '21 20:11 zxdawn

It doesn't look like it, there are a few suggestions above if you're interested in contributing this update @zxdawn.

greglucas avatar Nov 13 '21 15:11 greglucas

I've implemented a crude fix that works nicely if the axes share the same projection in EOmaps (a library for interactive cartopy-maps I'm developing)

Here's a stripped-down version of what I'm using ... maybe it helps to come up with a proper solution:

class joinaxes:
    def __init__(self):
        self.joined_action = False
        
    def __call__(self, ax1, ax2):
        
        cbx1, cby1 = self._get_callbacks(ax1)
        cbx2, cby2 = self._get_callbacks(ax2)

        ax2.callbacks.connect("xlim_changed", cbx1)
        ax2.callbacks.connect("ylim_changed", cby1)
       
        ax1.callbacks.connect("xlim_changed", cbx2)
        ax1.callbacks.connect("ylim_changed", cby2)

    def _get_callbacks(self, ax):
        def xlims_changed(event_ax):
            if not self.joined_action:
                self.joined_action = True
                ax.set_xlim(event_ax.get_xlim())
            self.joined_action = False
                
        def ylims_changed(event_ax):
            if not self.joined_action:
                self.joined_action = True
                ax.set_ylim(event_ax.get_ylim())
            self.joined_action = False
                
        return xlims_changed, ylims_changed


from cartopy import crs as ccrs
import matplotlib.pyplot as plt

f = plt.figure()
ax = f.add_subplot(211, projection=ccrs.PlateCarree())
ax.coastlines()
ax2 = f.add_subplot(212, projection=ccrs.PlateCarree())
ax2.coastlines()


joinaxes()(ax, ax2)

raphaelquast avatar Nov 23 '21 14:11 raphaelquast

@raphaelquast's solution worked for me - thanks!

PAGWatson avatar Dec 13 '21 10:12 PAGWatson

Is this fixed? The code from the OP now gives me

image

rcomer avatar Feb 17 '24 14:02 rcomer

@rcomer I did a quick check but for me (on v0.22.0) the issue still persists... (It might be able that the figure is initialized properly, but interactive pan/zoom still does not work as expected)

raphaelquast avatar Feb 18 '24 22:02 raphaelquast

Thanks @raphaelquast. What Matplotlib version do you have? I have mpl 3.8.2 and cannot reproduce what you see with either TkAgg or QtAgg. (My attempts to make a video failed so I can't post for comparison).

rcomer avatar Feb 19 '24 09:02 rcomer

Turns out the screencast fix was very googlable. Here is what I'm seeing:

https://github.com/SciTools/cartopy/assets/10599679/b5d8adc1-55d7-4972-9be5-df45725f04ef

rcomer avatar Feb 19 '24 13:02 rcomer

@rcomer Indeed, I was still at matplotlib 3.7.2 in the env I've used for checking... sorry for that... With mpl 3.8.2 I get the same as you so it really seems to be fixed!

raphaelquast avatar Feb 19 '24 13:02 raphaelquast

Thanks for checking @raphaelquast! I'll close this one then. :tada:

rcomer avatar Feb 19 '24 13:02 rcomer

Just a general note here because I think it's relevant... It might be a good idea to either issue a warning or completely disallow sharing axes with different projections... (or make sure to reproject limits prior to sharing ... but this might cause troubles especially for non-global projections)

raphaelquast avatar Feb 19 '24 13:02 raphaelquast