Repository: proplot-dev/proplot
Issue: x axis labeling disappears when using twinx()

#### Code sample, a copy-pastable example if possible
A "[Minimal, Complete and Verifiable Example](http://matthewrocklin.com/blog/work/2018/02/28/minimal-bug-reports)" will make it much easier for maintainers to help you.

```python
import proplot as plot
import numpy as np
import xarray as xr

times = np.arange(1990, 2000)
data = xr.DataArray(np.random.rand(len(times)), dims=['time'], coords=[times])

f, ax = plot.subplots(aspect=2)
ax.plot(data.time, data)

twinax = ax.twinx()

# twinax.format(xlocator=1)
ax.format(xticks=np.arange(1990, 2000, 2))
```

#### Actual result vs. expected result

Using `twinx()`, the tick labeling disappears on the twin axis. This happens for datetime and normal numbers. Using `xticks`/`xlocator` on the main axis or `twinax` instance changes the ticks but does not produce labels.


Changed files:
proplot/axes/base.py, proplot/axes/geo.py, proplot/figure.py, proplot/ui.py, proplot/axes/cartesian.py

diff --git a/proplot/axes/base.py b/proplot/axes/base.py
index 7d5e5177..027808f3 100644
--- a/proplot/axes/base.py
+++ b/proplot/axes/base.py
@@ -105,12 +105,12 @@ class Axes(maxes.Axes):
         self._left_panels = []
         self._right_panels = []
         self._tightbbox = None  # bounding boxes are saved
+        self._panel_hidden = False  # True when "filled" with cbar/legend
         self._panel_parent = None
-        self._panel_side = None
         self._panel_share = False
-        self._panel_hidden = False  # True when "filled" with cbar/legend
-        self._sharex_override = False
-        self._sharey_override = False
+        self._panel_sharex_group = False
+        self._panel_sharey_group = False
+        self._panel_side = None
         self._inset_parent = None
         self._inset_zoom = False
         self._inset_zoom_data = None
@@ -174,9 +174,63 @@ class Axes(maxes.Axes):
 
         # Automatic axis sharing and formatting
         # TODO: Instead of format() call specific setters
-        self._share_setup()
+        self._auto_share_setup()
         self.format(rc_mode=1)  # mode == 1 applies the rcShortParams
 
+    def _auto_share_setup(self):
+        """
+        Automatically configure axis sharing based on the horizontal and
+        vertical extent of subplots in the figure gridspec.
+        """
+        # Panel axes sharing, between main subplot and its panels
+        # NOTE: _panel_share means "include this panel in the axis sharing group"
+        # while _panel_sharex_group indicates the group itself and may include main axes
+        def shared(paxs):
+            return [pax for pax in paxs if not pax._panel_hidden and pax._panel_share]
+
+        # Internal axis sharing, share stacks of panels and main axes with each other
+        # NOTE: *This* block is why, even though share[xy] are figure-wide
+        # settings, we still need the axes-specific _share[xy]_override attr
+        if not self._panel_side:  # this is a main axes
+            # Top and bottom
+            bottom = self
+            paxs = shared(self._bottom_panels)
+            if paxs:
+                bottom = paxs[-1]
+                bottom._panel_sharex_group = False
+                for iax in (self, *paxs[:-1]):
+                    iax._panel_sharex_group = True
+                    iax._sharex_setup(bottom)  # parent is bottom-most
+            paxs = shared(self._top_panels)
+            for iax in paxs:
+                iax._panel_sharex_group = True
+                iax._sharex_setup(bottom)
+            # Left and right
+            # NOTE: Order of panel lists is always inside-to-outside
+            left = self
+            paxs = shared(self._left_panels)
+            if paxs:
+                left = paxs[-1]
+                left._panel_sharey_group = False
+                for iax in (self, *paxs[:-1]):
+                    iax._panel_sharey_group = True
+                    iax._sharey_setup(left)  # parent is left-most
+            paxs = shared(self._right_panels)
+            for iax in paxs:
+                iax._panel_sharey_group = True
+                iax._sharey_setup(left)
+
+        # External axes sharing, sometimes overrides panel axes sharing
+        # NOTE: This can get very repetitive, but probably minimal impact?
+        # Share x axes
+        parent, *children = self._get_extent_axes('x')
+        for child in children:
+            child._sharex_setup(parent)
+        # Share y axes
+        parent, *children = self._get_extent_axes('y')
+        for child in children:
+            child._sharey_setup(parent)
+
     def _draw_auto_legends_colorbars(self):
         """
         Generate automatic legends and colorbars. Wrapper funcs
@@ -259,10 +313,21 @@ class Axes(maxes.Axes):
         self.patch.set_alpha(0)
         self._panel_hidden = True
 
+    def _is_panel(self):
+        """
+        Return whether the current axes is a panel.
+        """
+        return bool(self._panel_parent)
+
+    def _is_panel_parent_or_child(self, other):
+        """
+        Return whether the axes are related.
+        """
+        return self._panel_parent is other or other._panel_parent is self
+
     def _loc_translate(self, loc, mode=None, allow_manual=True):
         """
-        Return the location string `loc` translated into a standardized
-        form.
+        Return the location string `loc` translated into a standardized form.
         """
         if mode == 'legend':
             valid = tuple(LOC_TRANSLATE.values())
@@ -357,7 +422,7 @@ class Axes(maxes.Axes):
         else:
             return bbox.ymin, bbox.ymax
 
-    def _reassign_suplabel(self, side):
+    def _reassign_subplot_label(self, side):
         """
         Re-assign the column and row labels to the relevant panel if
         present. This is called by `~proplot.figure.Figure._align_suplabel`.
@@ -422,9 +487,9 @@ class Axes(maxes.Axes):
         Configure x-axis sharing for panels. Main axis sharing is done in
         `~CartesianAxes._sharex_setup`.
         """
-        self._share_short_axis(sharex, 'left')
+        self._share_short_axis(sharex, 'left')  # x axis of left panels
         self._share_short_axis(sharex, 'right')
-        self._share_long_axis(sharex, 'bottom')
+        self._share_long_axis(sharex, 'bottom')  # x axis of bottom panels
         self._share_long_axis(sharex, 'top')
 
     def _sharey_setup(self, sharey):
@@ -432,72 +497,11 @@ class Axes(maxes.Axes):
         Configure y-axis sharing for panels. Main axis sharing is done in
         `~CartesianAxes._sharey_setup`.
         """
-        self._share_short_axis(sharey, 'bottom')
+        self._share_short_axis(sharey, 'bottom')  # y axis of bottom panels
         self._share_short_axis(sharey, 'top')
-        self._share_long_axis(sharey, 'left')
+        self._share_long_axis(sharey, 'left')  # y axis of left panels
         self._share_long_axis(sharey, 'right')
 
-    def _share_setup(self):
-        """
-        Automatically configure axis sharing based on the horizontal and
-        vertical extent of subplots in the figure gridspec.
-        """
-        # Panel axes sharing, between main subplot and its panels
-        # NOTE: While _panel_share just means "include this panel" in the
-        # axis sharing between the main subplot and panel children,
-        # _sharex_override and _sharey_override say "override the sharing level
-        # for these axes, because they belong to a panel group", and may
-        # include the main subplot itself.
-        def shared(paxs):
-            return [
-                pax for pax in paxs
-                if not pax._panel_hidden and pax._panel_share
-            ]
-
-        # Internal axis sharing; share stacks of panels and the main
-        # axes with each other.
-        # NOTE: *This* block is why, even though share[xy] are figure-wide
-        # settings, we still need the axes-specific _share[xy]_override attr
-        if not self._panel_side:  # this is a main axes
-            # Top and bottom
-            bottom = self
-            paxs = shared(self._bottom_panels)
-            if paxs:
-                bottom = paxs[-1]
-                bottom._sharex_override = False
-                for iax in (self, *paxs[:-1]):
-                    iax._sharex_override = True
-                    iax._sharex_setup(bottom)  # parent is bottom-most
-            paxs = shared(self._top_panels)
-            for iax in paxs:
-                iax._sharex_override = True
-                iax._sharex_setup(bottom)
-            # Left and right
-            # NOTE: Order of panel lists is always inside-to-outside
-            left = self
-            paxs = shared(self._left_panels)
-            if paxs:
-                left = paxs[-1]
-                left._sharey_override = False
-                for iax in (self, *paxs[:-1]):
-                    iax._sharey_override = True
-                    iax._sharey_setup(left)  # parent is left-most
-            paxs = shared(self._right_panels)
-            for iax in paxs:
-                iax._sharey_override = True
-                iax._sharey_setup(left)
-
-        # Main axes, sometimes overrides panel axes sharing
-        # TODO: This can get very repetitive, but probably minimal impact?
-        # Share x axes
-        parent, *children = self._get_extent_axes('x')
-        for child in children:
-            child._sharex_setup(parent)
-        # Share y axes
-        parent, *children = self._get_extent_axes('y')
-        for child in children:
-            child._sharey_setup(parent)
-
     def _share_short_axis(self, share, side):
         """
         Share the "short" axes of panels belonging to this subplot
@@ -528,38 +532,6 @@ class Axes(maxes.Axes):
         for pax in paxs:
             getattr(pax, '_share' + axis + '_setup')(share)
 
-    def _update_axis_labels(self, x='x', **kwargs):
-        """
-        Apply axis labels to the relevant shared axis. If spanning
-        labels are toggled this keeps the labels synced for all subplots in
-        the same row or column. Label positions will be adjusted at draw-time
-        with figure._align_axislabels.
-        """
-        # TODO: Major issues with this algorithm, especially when twin axes
-        # are present.
-        # TODO: Share axis locators and formatters just like matplotlib shares
-        # axis limits and proplot shares axis labels.
-        if x not in 'xy':
-            return
-
-        # Update label on this axes
-        axis = getattr(self, x + 'axis')
-        axis.label.update(kwargs)
-        kwargs.pop('color', None)
-        if getattr(self.figure, '_share' + x) == 0:
-            return
-
-        # Get "shared axes" siblings
-        ax, *_ = self._get_extent_axes(x, panels=True)
-        axs = [ax]
-        if getattr(ax.figure, '_span' + x):
-            side = axis.get_label_position()
-            if side in ('left', 'bottom'):
-                axs = ax._get_side_axes(side, panels=True)
-        for ax in axs:
-            axis = getattr(ax, x + 'axis')
-            axis.label.update(kwargs)  # apply to main axes
-
     def _update_title_position(self, renderer):
         """
         Update the position of proplot inset titles and builtin matplotlib
@@ -775,7 +747,7 @@ optional
                 'fontfamily': 'font.family'
             }, context=True)
         if suptitle or kw:
-            fig._update_figtitle(suptitle, **kw)
+            fig._update_super_title(suptitle, **kw)
 
         # Labels
         rlabels = _not_none(rightlabels=rightlabels, rlabels=rlabels)
@@ -797,7 +769,7 @@ optional
                 'fontfamily': 'font.family'
             }, context=True)
             if labels or kw:
-                fig._update_labels(self, side, labels, **kw)
+                fig._update_subplot_labels(self, side, labels, **kw)
 
         # Helper function
         def sanitize_kw(kw, loc):
@@ -1320,7 +1292,7 @@ optional
         """
         obj = self.pcolormesh(*args, **kwargs)
         aspect = _not_none(aspect, rc['image.aspect'])
-        xlocator, ylocator = None, None
+        xlocator = ylocator = None
         if hasattr(obj, '_coordinates'):
             coords = obj._coordinates
             coords = (coords[1:, ...] + coords[:-1, ...]) / 2
@@ -1475,7 +1447,7 @@ optional
                 line.set_visible(visible)
                 line_prev.set_visible(False)
         self._inset_zoom_data = (rectpatch, connects)
-        return (rectpatch, connects)
+        return rectpatch, connects
 
     def panel_axes(self, side, **kwargs):
         """
@@ -1542,9 +1514,8 @@ optional
             number of additional color levels between the line joints
             and the halfway points between line joints.
         scalex, scaley : bool, optional
-            These parameters determine if the view limits are adapted to
-            the data limits. The values are passed on to
-            `~matplotlib.axes.Axes.autoscale_view`.
+            Whether the view limits are adapted to the data limits. The values are
+            passed on to `~matplotlib.axes.Axes.autoscale_view`.
 
         Other parameters
         ----------------
@@ -1587,17 +1558,20 @@ optional
             coords, cmap=cmap, norm=norm,
             linestyles='-', capstyle='butt', joinstyle='miter',
         )
-        hs.set_array(np.array(values))
+        values = np.asarray(values)
+        hs.set_array(values)
         hs.update({
             key: value for key, value in kwargs.items()
             if key not in ('color',)
         })
 
         # Add collection with some custom attributes
+        # NOTE: Modern API uses self._request_autoscale_view but this is
+        # backwards compatible to earliest matplotlib versions.
         self.add_collection(hs)
         self.autoscale_view(scalex=scalex, scaley=scaley)
         hs.values = values
-        hs.levels = levels  # needed for other functions some
+        hs.levels = levels  # needed for other functions
         return hs
 
     def violins(self, *args, **kwargs):
diff --git a/proplot/axes/cartesian.py b/proplot/axes/cartesian.py
index 1033998a..be29d233 100644
--- a/proplot/axes/cartesian.py
+++ b/proplot/axes/cartesian.py
@@ -226,8 +226,9 @@ class CartesianAxes(base.Axes):
         """
         # Unlike matplotlib API, we strong arm user into certain twin axes
         # settings... doesn't really make sense to have twin axes without this
+        # NOTE: Could also use _panel_sharey_group = True to hide xaxis content
+        # but instead we set entire axis to visible = False. Safer that way.
         if self._altx_child is not None:  # altx was called on this axes
-            self._shared_y_axes.join(self, self._altx_child)
             self.spines['top'].set_visible(False)
             self.spines['bottom'].set_visible(True)
             self.xaxis.tick_bottom()
@@ -247,7 +248,6 @@ class CartesianAxes(base.Axes):
         Apply alternate *y* axis overrides.
         """
         if self._alty_child is not None:
-            self._shared_x_axes.join(self, self._alty_child)
             self.spines['right'].set_visible(False)
             self.spines['left'].set_visible(True)
             self.yaxis.tick_left()
@@ -262,6 +262,33 @@ class CartesianAxes(base.Axes):
             self.xaxis.set_visible(False)
             self.patch.set_visible(False)
 
+    def _apply_axis_sharing(self):
+        """
+        Enforce the "shared" axis labels and axis tick labels. If this is not called
+        at drawtime, "shared" labels can be inadvertantly turned off.
+        """
+        # X axis
+        axis = self.xaxis
+        if self._sharex is not None:
+            level = 3 if self._panel_sharex_group else self.figure._sharex
+            if level > 0:
+                axis.label.set_visible(False)
+            if level > 2:
+                # WARNING: Cannot set NullFormatter because shared axes share the
+                # same axis.Ticker classes. Instead use the approach copied from
+                # matplotlib subplots().
+                axis.set_tick_params(which='both', labelbottom=False, labeltop=False)
+
+        # Y axis
+        axis = self.yaxis
+        if self._sharey is not None:
+            level = 3 if self._panel_sharey_group else self.figure._sharey
+            if level > 0:
+                axis.label.set_visible(False)
+            if level > 2:
+                axis.set_tick_params(which='both', labelleft=False, labelright=False)
+        axis.set_minor_formatter(mticker.NullFormatter())
+
     def _datex_rotate(self):
         """
         Apply default rotation to datetime axis coordinates.
@@ -335,43 +362,6 @@ class CartesianAxes(base.Axes):
         child.set_ylim(nlim, emit=False)
         self._dualy_cache = (scale, *olim)
 
-    def _hide_labels(self):
-        """
-        Enforce the "shared" axis labels and axis tick labels. If this is
-        not called at drawtime, "shared" labels can be inadvertantly turned
-        off e.g. when the axis scale is changed.
-        """
-        for x in 'xy':
-            # "Shared" axis and tick labels
-            axis = getattr(self, x + 'axis')
-            share = getattr(self, '_share' + x)
-            if share is not None:
-                level = (
-                    3 if getattr(self, '_share' + x + '_override')
-                    else getattr(self.figure, '_share' + x)
-                )
-                if level > 0:
-                    axis.label.set_visible(False)
-                if level > 2:
-                    axis.set_major_formatter(mticker.NullFormatter())
-            # Enforce no minor ticks labels. TODO: Document?
-            axis.set_minor_formatter(mticker.NullFormatter())
-
-    def _make_twin_axes(self, *args, **kwargs):
-        """
-        Return a twin of this axes. This is used for twinx and twiny and was
-        copied from matplotlib in case the private API changes.
-        """
-        # Typically, SubplotBase._make_twin_axes is called instead of this.
-        # There is also an override in axes_grid1/axes_divider.py.
-        if 'sharex' in kwargs and 'sharey' in kwargs:
-            raise ValueError('Twinned Axes may share only one axis.')
-        ax2 = self.figure.add_axes(self.get_position(True), *args, **kwargs)
-        self.set_adjustable('datalim')
-        ax2.set_adjustable('datalim')
-        self._twinned_axes.join(self, ax2)
-        return ax2
-
     def _sharex_setup(self, sharex):
         """
         Configure shared axes accounting for panels. The input is the
@@ -380,7 +370,10 @@ class CartesianAxes(base.Axes):
         # Share panel across different subplots
         super()._sharex_setup(sharex)
         # Get sharing level
-        level = 3 if self._sharex_override else self.figure._sharex
+        level = (
+            3 if self._panel_sharex_group and self._is_panel_parent_or_child(sharex)
+            else self.figure._sharex
+        )
         if level not in range(4):
             raise ValueError(
                 'Invalid sharing level sharex={value!r}. '
@@ -394,8 +387,13 @@ class CartesianAxes(base.Axes):
         # Builtin sharing features
         if level > 0:
             self._sharex = sharex
+            text = self.xaxis.label.get_text()
+            if text:
+                sharex.xaxis.label.set_text(text)
         if level > 1:
             self._shared_x_axes.join(self, sharex)
+            self.xaxis.major = sharex.xaxis.major
+            self.xaxis.minor = sharex.xaxis.minor
 
     def _sharey_setup(self, sharey):
         """
@@ -405,7 +403,10 @@ class CartesianAxes(base.Axes):
         # Share panel across different subplots
         super()._sharey_setup(sharey)
         # Get sharing level
-        level = 3 if self._sharey_override else self.figure._sharey
+        level = (
+            3 if self._panel_sharey_group and self._is_panel_parent_or_child(sharey)
+            else self.figure._sharey
+        )
         if level not in range(4):
             raise ValueError(
                 'Invalid sharing level sharey={value!r}. '
@@ -419,8 +420,47 @@ class CartesianAxes(base.Axes):
         # Builtin features
         if level > 0:
             self._sharey = sharey
+            text = self.yaxis.label.get_text()
+            if text:
+                sharey.yaxis.label.set_text(text)
         if level > 1:
             self._shared_y_axes.join(self, sharey)
+            self.yaxis.major = sharey.yaxis.major
+            self.yaxis.minor = sharey.yaxis.minor
+
+    def _update_axis_labels(self, x='x', **kwargs):
+        """
+        Apply axis labels to the relevant shared axis. If spanning labels are toggled
+        this keeps the labels synced for all subplots in the same row or column. Label
+        positions will be adjusted at draw-time with figure._align_axislabels.
+        """
+        if x not in 'xy':
+            return
+
+        # Get axes in 3 step process
+        # 1. Walk to parent if it is a main axes
+        # 2. Get spanning main axes in this row or column (ignore short panel edges)
+        # 3. Walk to parent if it exists (may be a panel long edge)
+        # NOTE: Critical to apply labels to *shared* axes attributes rather
+        # than testing extents or we end up sharing labels with twin axes.
+        ax = self
+        if getattr(self.figure, '_share' + x) > 0:
+            share = getattr(ax, '_share' + x) or ax
+            if not share._panel_parent:
+                ax = share  # shared main axes are only ever one level deep
+
+        # Get spanning axes
+        axs = [ax]
+        if getattr(ax.figure, '_span' + x):
+            side = getattr(self, x + 'axis').get_label_position()
+            if side in ('left', 'bottom'):
+                axs = ax._get_side_axes(side, panels=False)
+
+        # Update axes with label
+        for ax in axs:
+            ax = getattr(ax, '_share' + x) or ax  # defer to panel
+            axis = getattr(ax, x + 'axis')
+            axis.label.update(kwargs)
 
     def format(
         self, *,
@@ -645,30 +685,18 @@ class CartesianAxes(base.Axes):
             # Flexible keyword args, declare defaults
             xmargin = _not_none(xmargin, rc.get('axes.xmargin', context=True))
             ymargin = _not_none(ymargin, rc.get('axes.ymargin', context=True))
-            xtickdir = _not_none(
-                xtickdir, rc.get('xtick.direction', context=True)
-            )
-            ytickdir = _not_none(
-                ytickdir, rc.get('ytick.direction', context=True)
-            )
+            xtickdir = _not_none(xtickdir, rc.get('xtick.direction', context=True))
+            ytickdir = _not_none(ytickdir, rc.get('ytick.direction', context=True))
+            xformatter = _not_none(xticklabels=xticklabels, xformatter=xformatter)
+            yformatter = _not_none(yticklabels=yticklabels, yformatter=yformatter)
+            xlocator = _not_none(xticks=xticks, xlocator=xlocator)
+            ylocator = _not_none(yticks=yticks, ylocator=ylocator)
             xtickminor = _not_none(
                 xtickminor, rc.get('xtick.minor.visible', context=True)
             )
             ytickminor = _not_none(
                 ytickminor, rc.get('ytick.minor.visible', context=True)
             )
-            xformatter = _not_none(
-                xticklabels=xticklabels, xformatter=xformatter,
-            )
-            yformatter = _not_none(
-                yticklabels=yticklabels, yformatter=yformatter,
-            )
-            xlocator = _not_none(
-                xticks=xticks, xlocator=xlocator
-            )
-            ylocator = _not_none(
-                yticks=yticks, ylocator=ylocator
-            )
             xminorlocator = _not_none(
                 xminorticks=xminorticks, xminorlocator=xminorlocator
             )
@@ -705,24 +733,12 @@ class CartesianAxes(base.Axes):
             # Sensible defaults for spine, tick, tick label, and label locs
             # NOTE: Allow tick labels to be present without ticks! User may
             # want this sometimes! Same goes for spines!
-            xspineloc = _not_none(
-                xloc=xloc, xspineloc=xspineloc,
-            )
-            yspineloc = _not_none(
-                yloc=yloc, yspineloc=yspineloc,
-            )
-            xtickloc = _not_none(
-                xtickloc, xspineloc, _parse_rcloc('x', 'xtick')
-            )
-            ytickloc = _not_none(
-                ytickloc, yspineloc, _parse_rcloc('y', 'ytick')
-            )
-            xspineloc = _not_none(
-                xspineloc, _parse_rcloc('x', 'axes.spines')
-            )
-            yspineloc = _not_none(
-                yspineloc, _parse_rcloc('y', 'axes.spines')
-            )
+            xspineloc = _not_none(xloc=xloc, xspineloc=xspineloc,)
+            yspineloc = _not_none(yloc=yloc, yspineloc=yspineloc,)
+            xtickloc = _not_none(xtickloc, xspineloc, _parse_rcloc('x', 'xtick'))
+            ytickloc = _not_none(ytickloc, yspineloc, _parse_rcloc('y', 'ytick'))
+            xspineloc = _not_none(xspineloc, _parse_rcloc('x', 'axes.spines'))
+            yspineloc = _not_none(yspineloc, _parse_rcloc('y', 'axes.spines'))
             if xtickloc != 'both':
                 xticklabelloc = _not_none(xticklabelloc, xtickloc)
                 xlabelloc = _not_none(xlabelloc, xticklabelloc)
@@ -1059,11 +1075,7 @@ class CartesianAxes(base.Axes):
                 # NOTE: The only reliable way to disable ticks labels and then
                 # restore them is by messing with the *formatter*, rather than
                 # setting labelleft=False, labelright=False, etc.
-                if (formatter is not None or tickrange is not None) and not (
-                    isinstance(
-                        axis.get_major_formatter(), mticker.NullFormatter
-                    ) and getattr(self, '_share' + x)
-                ):
+                if formatter is not None or tickrange is not None:
                     # Tick range
                     if tickrange is not None:
                         if formatter not in (None, 'auto'):
@@ -1185,24 +1197,24 @@ class CartesianAxes(base.Axes):
         # Perform extra post-processing steps
         # NOTE: This mimics matplotlib API, which calls identical
         # post-processing steps in both draw() and get_tightbbox()
-        self._hide_labels()
         self._altx_overrides()
         self._alty_overrides()
         self._dualx_overrides()
         self._dualy_overrides()
         self._datex_rotate()
+        self._apply_axis_sharing()
         if self._inset_parent is not None and self._inset_zoom:
             self.indicate_inset_zoom()
         super().draw(renderer, *args, **kwargs)
 
     def get_tightbbox(self, renderer, *args, **kwargs):
         # Perform extra post-processing steps
-        self._hide_labels()
         self._altx_overrides()
         self._alty_overrides()
         self._dualx_overrides()
         self._dualy_overrides()
         self._datex_rotate()
+        self._apply_axis_sharing()
         if self._inset_parent is not None and self._inset_zoom:
             self.indicate_inset_zoom()
         return super().get_tightbbox(renderer, *args, **kwargs)
diff --git a/proplot/axes/geo.py b/proplot/axes/geo.py
index 79fa6615..7e7826d5 100644
--- a/proplot/axes/geo.py
+++ b/proplot/axes/geo.py
@@ -711,7 +711,7 @@ class CartopyAxes(GeoAxes, GeoAxesCartopy):
         self.background_patch.update(kw_face)
         self.outline_patch.update(kw_edge)
 
-    def _hide_labels(self):
+    def _apply_axis_sharing(self):
         """
         No-op for now. In future this will hide meridian and parallel
         labels for rectangular projections with axis sharing.
@@ -721,7 +721,7 @@ class CartopyAxes(GeoAxes, GeoAxesCartopy):
     def get_tightbbox(self, renderer, *args, **kwargs):
         # Perform extra post-processing steps
         # For now this just draws the gridliners
-        self._hide_labels()
+        self._apply_axis_sharing()
         if self.get_autoscale_on() and self.ignore_existing_data_limits:
             self.autoscale_view()
         if (
diff --git a/proplot/figure.py b/proplot/figure.py
index ac35ffeb..49cd0504 100644
--- a/proplot/figure.py
+++ b/proplot/figure.py
@@ -340,7 +340,8 @@ class Figure(mfigure.Figure):
 
         # Axis sharing and axis setup only for non-legend or colorbar axes
         if not filled:
-            ax._share_setup()
+            for ax in self._axes_main:
+                ax._auto_share_setup()
             axis = pax.yaxis if side in ('left', 'right') else pax.xaxis
             getattr(axis, 'tick_' + side)()  # set tick and label positions
             axis.set_label_position(side)
@@ -458,7 +459,7 @@ class Figure(mfigure.Figure):
         # NOTE: Need to turn off aligned labels before
         # _update_geometry_from_tight_layout
         # call, so cannot put this inside Axes draw
-        tracker = {*()}
+        xaxs_updated = set()
         for ax in self._axes_main:
             if not isinstance(ax, paxes.CartesianAxes):
                 continue
@@ -466,55 +467,58 @@ class Figure(mfigure.Figure):
                 side = axis.get_label_position()
                 span = getattr(self, '_span' + x)
                 align = getattr(self, '_align' + x)
-                if side not in ('bottom', 'left') or axis in tracker:
+                if side not in ('bottom', 'left') or axis in xaxs_updated:
                     continue
-                axs = ax._get_side_axes(side)
-                for _ in range(2):
-                    axs = [getattr(ax, '_share' + x) or ax for ax in axs]
+
+                # Get panels for axes on each side (2 levels deep is maximum)
+                axs = ax._get_side_axes(side, panels=False)
+                axs = [getattr(ax, '_share' + x) or ax for ax in axs]
+                axs = [getattr(ax, '_share' + x) or ax for ax in axs]
 
                 # Align axis label offsets
-                axises = [getattr(ax, x + 'axis') for ax in axs]
-                tracker.update(axises)
+                xaxs = [getattr(ax, x + 'axis') for ax in axs]
+                xaxs_updated.update(xaxs)
                 if span or align:
-                    grp = getattr(self, '_align_' + x + 'label_grp', None)
-                    if grp is not None:
+                    group = getattr(self, '_align_' + x + 'label_grp', None)
+                    if group is not None:
                         for ax in axs[1:]:
-                            # copied from source code, add to grouper
-                            grp.join(axs[0], ax)
+                            group.join(axs[0], ax)  # add to grouper
                     elif align:
                         warnings._warn_proplot(
-                            'Aligning *x* and *y* axis labels required '
+                            'Aligning *x* and *y* axis labels requires '
                             'matplotlib >=3.1.0'
                         )
                 if not span:
                     continue
 
                 # Get spanning label position
-                c, spanax = self._get_align_coord(side, axs)
-                spanaxis = getattr(spanax, x + 'axis')
-                spanlabel = spanaxis.label
-                if not hasattr(spanlabel, '_orig_transform'):
-                    spanlabel._orig_transform = spanlabel.get_transform()
-                    spanlabel._orig_position = spanlabel.get_position()
+                c, ax_span = self._get_align_coord(side, axs)
+                ax_span = getattr(ax_span, '_share' + x) or ax_span
+                ax_span = getattr(ax_span, '_share' + x) or ax_span
+                axis_span = getattr(ax_span, x + 'axis')
+                label_span = axis_span.label
+                if not hasattr(label_span, '_orig_transform'):
+                    label_span._orig_transform = label_span.get_transform()
+                    label_span._orig_position = label_span.get_position()
                 if not b:  # toggle off, done before tight layout
-                    spanlabel.set_transform(spanlabel._orig_transform)
-                    spanlabel.set_position(spanlabel._orig_position)
-                    for axis in axises:
+                    label_span.set_transform(label_span._orig_transform)
+                    label_span.set_position(label_span._orig_position)
+                    for axis in xaxs:
                         axis.label.set_visible(True)
                 else:  # toggle on, done after tight layout
                     if x == 'x':
                         position = (c, 1)
                         transform = mtransforms.blended_transform_factory(
-                            self.transFigure, mtransforms.IdentityTransform())
+                            self.transFigure, mtransforms.IdentityTransform()
+                        )
                     else:
                         position = (1, c)
                         transform = mtransforms.blended_transform_factory(
-                            mtransforms.IdentityTransform(), self.transFigure)
-                    for axis in axises:
-                        axis.label.set_visible((axis is spanaxis))
-                    spanlabel.update({
-                        'position': position, 'transform': transform
-                    })
+                            mtransforms.IdentityTransform(), self.transFigure
+                        )
+                    for axis in xaxs:
+                        axis.label.set_visible((axis is axis_span))
+                    label_span.update({'position': position, 'transform': transform})
 
     def _align_labels_figure(self, renderer):
         """
@@ -539,7 +543,7 @@ class Figure(mfigure.Figure):
                 x = 'y'
                 panels = ('left', 'right')
             axs = self._get_align_axes(side)
-            axs = [ax._reassign_suplabel(side) for ax in axs]
+            axs = [ax._reassign_subplot_label(side) for ax in axs]
             labels = [getattr(ax, '_' + side + '_label') for ax in axs]
             coords = [None] * len(axs)
             if side == 'top' and suptitle_on:
@@ -660,22 +664,22 @@ class Figure(mfigure.Figure):
                 for iax in ax._iter_axes(panels=panels, children=False)
             ]
 
-        # Get coords
+        # Get coordinates
         ranges = np.array([ax._range_gridspec(x) for ax in axs])
         min_, max_ = ranges[:, 0].min(), ranges[:, 1].max()
-        axlo = axs[np.where(ranges[:, 0] == min_)[0][0]]
-        axhi = axs[np.where(ranges[:, 1] == max_)[0][0]]
-        lobox = axlo.get_subplotspec().get_position(self)
-        hibox = axhi.get_subplotspec().get_position(self)
+        ax_lo = axs[np.where(ranges[:, 0] == min_)[0][0]]
+        ax_hi = axs[np.where(ranges[:, 1] == max_)[0][0]]
+        box_lo = ax_lo.get_subplotspec().get_position(self)
+        box_hi = ax_hi.get_subplotspec().get_position(self)
         if x == 'x':
-            pos = 0.5 * (lobox.x0 + hibox.x1)
+            pos = 0.5 * (box_lo.x0 + box_hi.x1)
         else:
-            pos = 0.5 * (lobox.y1 + hibox.y0)  # 'lo' is actually on top, highest in gs
+            pos = 0.5 * (box_lo.y1 + box_hi.y0)  # 'lo' is actually on top of figure
 
         # Return axis suitable for spanning position
-        spanax = axs[(np.argmin(ranges[:, 0]) + np.argmax(ranges[:, 1])) // 2]
-        spanax = spanax._panel_parent or spanax
-        return pos, spanax
+        ax_span = axs[(np.argmin(ranges[:, 0]) + np.argmax(ranges[:, 1])) // 2]
+        ax_span = ax_span._panel_parent or ax_span
+        return pos, ax_span
 
     def _get_align_axes(self, side):
         """
@@ -776,7 +780,6 @@ class Figure(mfigure.Figure):
         # Update figure
         figsize, gridspec_kw, _ = pgridspec._calc_geometry(**subplots_kw)
         if slot_exists:
-            # Use existing gridspec
             gridspec = self._gridspec_main
             gridspec.update(**gridspec_kw)
 
@@ -823,7 +826,7 @@ class Figure(mfigure.Figure):
 
         return gridspec
 
-    def _update_figtitle(self, title, **kwargs):
+    def _update_super_title(self, title, **kwargs):
         """
         Assign the figure "super title" and update settings.
         """
@@ -832,7 +835,7 @@ class Figure(mfigure.Figure):
         if kwargs:
             self._suptitle.update(kwargs)
 
-    def _update_labels(self, ax, side, labels, **kwargs):
+    def _update_subplot_labels(self, ax, side, labels, **kwargs):
         """
         Assign the side labels and update settings.
         """
@@ -1232,23 +1235,25 @@ class Figure(mfigure.Figure):
         else:
             width, height = w, h
         if not all(np.isfinite(_) for _ in (width, height)):
-            raise ValueError(
-                f'Figure size must be finite, not ({width}, {height}).'
-            )
+            raise ValueError(f'Figure size must be finite, not ({width}, {height}).')
         width_true, height_true = self.get_size_inches()
         width_trunc = int(self.bbox.width) / self.dpi
         height_trunc = int(self.bbox.height) / self.dpi
-        if (
-            (
+        user = (  # detect user resize
+            (  # sometimes get (width_trunc, height_true) or (width_true, height_trunc)
                 width not in (width_true, width_trunc)
                 or height not in (height_true, height_trunc)
             )
             and not auto
             and not self._is_autoresizing
-            and not getattr(self.canvas, '_is_idle_drawing', None)  # standard
-        ):
+            and not self._is_preprocessing
+            and not getattr(self.canvas, '_is_idle_drawing', None)
+            and not getattr(self.canvas, '_is_drawing', None)
+            and not getattr(self.canvas, '_draw_pending', None)
+        )
+        if user:
             self._subplots_kw.update(width=width, height=height)
-        context = self._context_autoresizing if auto else _dummy_context
+        context = self._context_autoresizing if auto or not user else _dummy_context
         with context():
             super().set_size_inches(width, height, forward=forward)
 
diff --git a/proplot/ui.py b/proplot/ui.py
index 58579dca..c997a80d 100644
--- a/proplot/ui.py
+++ b/proplot/ui.py
@@ -494,9 +494,9 @@ def subplots(
 
     # Shared axes setup
     # TODO: Figure out how to defer this to drawtime in #50
-    # For some reason just adding _share_setup() to draw() doesn't work
+    # For some reason just adding _auto_share_setup() to draw() doesn't work
     for ax in axs:
-        ax._share_setup()
+        ax._auto_share_setup()
 
     # Return figure and axes
     n = ncols if order == 'C' else nrows
@@ -659,17 +659,17 @@ class SubplotsContainer(list):
         elif all(callable(_) for _ in objs):
             @functools.wraps(objs[0])
             def _iterator(*args, **kwargs):
-                ret = []
+                result = []
                 for func in objs:
-                    ret.append(func(*args, **kwargs))
+                    result.append(func(*args, **kwargs))
                 if len(self) == 1:
-                    return ret[0]
-                elif all(res is None for res in ret):
+                    return result[0]
+                elif all(res is None for res in result):
                     return None
-                elif all(isinstance(res, paxes.Axes) for res in ret):
-                    return SubplotsContainer(ret, n=self._n, order=self._order)
+                elif all(isinstance(res, paxes.Axes) for res in result):
+                    return SubplotsContainer(result, n=self._n, order=self._order)
                 else:
-                    return tuple(ret)
+                    return tuple(result)
             _iterator.__doc__ = inspect.getdoc(objs[0])
             return _iterator