Skip to content

Commit

Permalink
Add axis sharing level '4' or 'all'
Browse files Browse the repository at this point in the history
  • Loading branch information
lukelbd committed Aug 22, 2021
1 parent d27d05c commit 73f355a
Show file tree
Hide file tree
Showing 3 changed files with 100 additions and 87 deletions.
17 changes: 10 additions & 7 deletions docs/layout.py
Original file line number Diff line number Diff line change
Expand Up @@ -294,13 +294,16 @@
# the axis labels, but nothing else.
# * ``share='limits'``, ``share='lims'``, or ``share=2`` is the same as ``1``,
# but also shares the axis limits, axis scales, and tick locations.
# * ``share=True`` or ``share=3`` is the same as ``2``,
# but also shares the axis tick labels.
#
# #. Adding an option to automatically share labels between axes spanning the same
# row or column of the subplot grid, controlled by the `spanx` and `spany`
# keywords (default is :rc:`subplots.span`). Use the `span` keyword as a
# shorthand to set both `spanx` and `spany`.
# * ``share=True`` or ``share=3`` is the same as ``2``, but also shares
# the axis tick labels.
# * ``share='all'`` or ``share=4`` is the same as ``3``, but also shares
# the axis limits, axis scales, and tick locations between subplots that
# are not in the same row or column.
#
# #. Adding an option to automatically share labels between axes spanning the
# same row or column of the subplot grid, controlled by the `spanx` and
# `spany` keywords (default is :rc:`subplots.span`). Use the `span` keyword
# as a shorthand to set both `spanx` and `spany`.
#
# The below examples demonstrate the effect of various axis and label sharing
# settings on the appearance of several subplot grids.
Expand Down
134 changes: 69 additions & 65 deletions proplot/axes/cartesian.py
Original file line number Diff line number Diff line change
Expand Up @@ -255,106 +255,110 @@ def _parse_alt(x, kwargs):
raise TypeError(f'Unexpected keyword argument(s): {kw_bad!r}')
return kw_out

def _sharex_limits(self, sharex):
"""
Safely share limits and tickers without resetting things.
"""
# Copy non-default limits and scales
# NOTE: If e.g. we make a top panel then 'sharex' will be the
# extant axes and 'self' is the new axes. But if e.g. we make a bottom
# panel then 'sharex' will be the new axes and 'self' the extant one.
for (ax1, ax2) in ((self, sharex), (sharex, self)):
if ax1.get_xscale() == 'linear' and ax2.get_xscale() != 'linear':
ax1.set_xscale(ax2.get_xscale()) # non-default scale
if ax1.get_autoscalex_on() and not ax2.get_autoscalex_on():
ax1.set_xlim(ax2.get_xlim()) # non-default limits
# Copy non-default locators and formatters
self._shared_x_axes.join(self, sharex) # share limit/scale changes
if sharex.xaxis.isDefault_majloc and not self.xaxis.isDefault_majloc:
sharex.xaxis.set_major_locator(self.xaxis.get_major_locator())
if sharex.xaxis.isDefault_minloc and not self.xaxis.isDefault_minloc:
sharex.xaxis.set_minor_locator(self.xaxis.get_minor_locator())
if sharex.xaxis.isDefault_majfmt and not self.xaxis.isDefault_majfmt:
sharex.xaxis.set_major_formatter(self.xaxis.get_major_formatter())
if sharex.xaxis.isDefault_minfmt and not self.xaxis.isDefault_minfmt:
sharex.xaxis.set_minor_formatter(self.xaxis.get_minor_formatter())
self.xaxis.major = sharex.xaxis.major
self.xaxis.minor = sharex.xaxis.minor

def _sharey_limits(self, sharey):
"""
Safely share limits and tickers without resetting things.
"""
# NOTE: See _sharex_limits for notes
for (ax1, ax2) in ((self, sharey), (sharey, self)):
if ax1.get_yscale() == 'linear' and ax2.get_yscale() != 'linear':
ax1.set_yscale(ax2.get_yscale())
if ax1.get_autoscaley_on() and not ax2.get_autoscaley_on():
ax1.set_ylim(ax2.get_ylim())
self._shared_y_axes.join(self, sharey) # share limit/scale changes
if sharey.yaxis.isDefault_majloc and not self.yaxis.isDefault_majloc:
sharey.yaxis.set_major_locator(self.yaxis.get_major_locator())
if sharey.yaxis.isDefault_minloc and not self.yaxis.isDefault_minloc:
sharey.yaxis.set_minor_locator(self.yaxis.get_minor_locator())
if sharey.yaxis.isDefault_majfmt and not self.yaxis.isDefault_majfmt:
sharey.yaxis.set_major_formatter(self.yaxis.get_major_formatter())
if sharey.yaxis.isDefault_minfmt and not self.yaxis.isDefault_minfmt:
sharey.yaxis.set_minor_formatter(self.yaxis.get_minor_formatter())
self.yaxis.major = sharey.yaxis.major
self.yaxis.minor = sharey.yaxis.minor

def _sharex_setup(self, sharex):
"""
Configure shared axes accounting for panels. The input is the
'parent' axes, from which this one will draw its properties.
"""
# Share panels across *different* subplots
super()._sharex_setup(sharex)

# Get sharing level
# Get the axis sharing level
level = (
3 if self._panel_sharex_group and self._is_panel_group_member(sharex)
else self.figure._sharex
)
if level not in range(4): # must be internal error
if level not in range(5): # must be internal error
raise ValueError(f'Invalid sharing level sharex={level!r}.')
if sharex in (None, self) or not isinstance(sharex, CartesianAxes):
return

# Share future changes to axis labels
# Proplot internally uses _sharex and _sharey for label sharing. Matplotlib
# only uses these in __init__() and cla() to share tickers -- all other builtin
# matplotlib axis sharing features derive from _shared_x_axes() group.
# Share future axis label changes. Implemented in _apply_axis_sharing().
# Matplotlib only uses these attributes in __init__() and cla() to share
# tickers -- all other builtin sharing features derives from _shared_x_axes
if level > 0:
self._sharex = sharex
if not sharex.xaxis.label.get_text():
self._transfer_text(self.xaxis.label, sharex.xaxis.label)

# Share future axis tickers, limits, and scales
# NOTE: Only difference between levels 2 and 3 is level 3 hides
# tick labels. But this is done after the fact -- tickers are still shared.
# NOTE: Only difference between levels 2 and 3 is level 3 hides tick
# labels. But this is done after the fact -- tickers are still shared.
if level > 1:
# Initial limits and scales should be shared both ways
for (ax1, ax2) in ((self, sharex), (sharex, self)):
if ax1.get_xscale() == 'linear' and ax2.get_xscale() != 'linear':
ax1.set_xscale(ax2.get_xscale())
if ax1.get_autoscalex_on() and not ax2.get_autoscalex_on():
ax1.set_xlim(ax2.get_xlim())

# Locators and formatters only need to be shared from children
# to parent, because this is done automatically when we assign
# parent sharex tickers to child.
self._shared_x_axes.join(self, sharex) # share limit/scale changes
if sharex.xaxis.isDefault_majloc and not self.xaxis.isDefault_majloc:
sharex.xaxis.set_major_locator(self.xaxis.get_major_locator())
if sharex.xaxis.isDefault_minloc and not self.xaxis.isDefault_minloc:
sharex.xaxis.set_minor_locator(self.xaxis.get_minor_locator())
if sharex.xaxis.isDefault_majfmt and not self.xaxis.isDefault_majfmt:
sharex.xaxis.set_major_formatter(self.xaxis.get_major_formatter())
if sharex.xaxis.isDefault_minfmt and not self.xaxis.isDefault_minfmt:
sharex.xaxis.set_minor_formatter(self.xaxis.get_minor_formatter())
self.xaxis.major = sharex.xaxis.major
self.xaxis.minor = sharex.xaxis.minor
self._sharex_limits(sharex)
# Share limits with the entire subplot grid. Use the reference axes as
# the 'base' because that seems to make sense. Although should not matter.
if level > 3 and self.number:
ref = self.figure._subplot_dict.get(self.figure._refnum, None)
if self is not ref:
self._sharex_limits(ref)

def _sharey_setup(self, sharey):
"""
Configure shared axes accounting for panels. The input is the
'parent' axes, from which this one will draw its properties.
"""
# Share *panels* across different subplots
# NOTE: See _sharex_setup for notes
super()._sharey_setup(sharey)

# Get sharing level
level = (
3 if self._panel_sharey_group and self._is_panel_group_member(sharey)
else self.figure._sharey
)
if level not in range(4): # must be internal error
if level not in range(5): # must be internal error
raise ValueError(f'Invalid sharing level sharey={level!r}.')
if sharey in (None, self) or not isinstance(sharey, CartesianAxes):
return

# Share future changes to axis labels
if level > 0:
self._sharey = sharey
if not sharey.yaxis.label.get_text():
sharey.yaxis.label.set_text(self.yaxis.label.get_text())

# Share future axis tickers, limits, and scales
if level > 1:
# Initial limits and scales should be shared both ways
for (ax1, ax2) in ((self, sharey), (sharey, self)):
if ax1.get_yscale() == 'linear' and ax2.get_yscale() != 'linear':
ax1.set_yscale(ax2.get_yscale())
if ax1.get_autoscaley_on() and not ax2.get_autoscaley_on():
ax1.set_ylim(ax2.get_ylim())

# Locators and formatters only need to be shared from children
# to parent, because this is done automatically when we assign
# parent sharey tickers to child.
self._shared_y_axes.join(self, sharey) # share limit/scale changes
if sharey.yaxis.isDefault_majloc and not self.yaxis.isDefault_majloc:
sharey.yaxis.set_major_locator(self.yaxis.get_major_locator())
if sharey.yaxis.isDefault_minloc and not self.yaxis.isDefault_minloc:
sharey.yaxis.set_minor_locator(self.yaxis.get_minor_locator())
if sharey.yaxis.isDefault_majfmt and not self.yaxis.isDefault_majfmt:
sharey.yaxis.set_major_formatter(self.yaxis.get_major_formatter())
if sharey.yaxis.isDefault_minfmt and not self.yaxis.isDefault_minfmt:
sharey.yaxis.set_minor_formatter(self.yaxis.get_minor_formatter())
self.yaxis.major = sharey.yaxis.major
self.yaxis.minor = sharey.yaxis.minor
self._sharey_limits(sharey)
if level > 3 and self.number:
ref = self.figure._subplot_dict.get(self.figure._refnum, None)
if self is not ref:
self._sharey_limits(ref)

def _update_bounds(self, x, fixticks=False):
"""
Expand Down
36 changes: 21 additions & 15 deletions proplot/figure.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,19 +97,21 @@
Aliases for `figwidth`, `figheight`.
figsize : length-2 tuple, optional
Tuple specifying the figure ``(width, height)``.
sharex, sharey, share : {0, False, 1, 'labels', 'labs', 2, 'limits', 'lims', 3, True}, \
optional
sharex, sharey, share \
: {0, False, 1, 'labels', 'labs', 2, 'limits', 'lims', 3, True, 4, 'all'}, optional
The axis sharing "level" for the *x* axis, *y* axis, or both axes.
Default is :rc:`subplots.share`. Options are as follows:
* ``0`` or ``False``: No axis sharing. This also sets the default `spanx`
and `spany` values to ``False``.
* ``1`` or ``'labels'`` or ``'labs'``: Only draw axis labels on the bottommost
row or leftmost column of subplots. Tick labels still appear on every subplot.
* ``2`` or ``'limits'`` or ``'lims'``: As above but force the axis limits to be
identical. Tick labels still appear on every subplot.
* ``2`` or ``'limits'`` or ``'lims'``: As above but force the axis limits, scales,
and tick locations to be identical. Tick labels still appear on every subplot.
* ``3`` or ``True``: As above but only show the tick labels on the bottommost
row and leftmost column of subplots.
* ``4`` or ``'all'``: As above but also share the axis limits, scales, and
tick locations between subplots not in the same row or column.
spanx, spany, span : bool or {0, 1}, optional
Whether to use "spanning" axis labels for the *x* axis, *y* axis, or both
Expand Down Expand Up @@ -476,8 +478,10 @@ class Figure(mfigure.Figure):
_share_message = (
'Axis sharing level can be 0 or False (share nothing), '
"1 or 'labels' or 'labs' (share axis labels), "
"2 or 'limits' or 'lims' (share axis limits and axis labels), or "
'3 or True (share axis limits, axis labels, and tick labels).'
"2 or 'limits' or 'lims' (share axis limits and axis labels), "
'3 or True (share axis limits, axis labels, and tick labels), '
"or 4 or 'all' (share axis labels and tick labels in the same gridspec "
'rows and columns and share axis limits across all subplots).'
)
_space_message = (
'To set the left, right, bottom, top, wspace, or hspace gridspec values, '
Expand Down Expand Up @@ -659,29 +663,31 @@ def __init__(
self._includepanels = _not_none(includepanels, False)

# Translate share settings
translate = {'labels': 1, 'labs': 1, 'limits': 2, 'lims': 2}
translate = {'labels': 1, 'labs': 1, 'limits': 2, 'lims': 2, 'all': 4}
sharex = _not_none(sharex, share, rc['subplots.share'])
sharey = _not_none(sharey, share, rc['subplots.share'])
sharex = 3 if sharex is True else translate.get(sharex, sharex)
sharey = 3 if sharey is True else translate.get(sharey, sharey)
if sharex not in range(4):
if sharex not in range(5):
raise ValueError(f'Invalid sharex={sharex!r}. ' + self._share_message)
if sharey not in range(4):
if sharey not in range(5):
raise ValueError(f'Invalid sharey={sharey!r}. ' + self._share_message)
self._sharex = int(sharex)
self._sharey = int(sharey)

# Translate span and align settings
spanx = _not_none(spanx, span, 0 if sharex == 0 else None, rc['subplots.span'])
spany = _not_none(spany, span, 0 if sharey == 0 else None, rc['subplots.span'])
spanx = _not_none(spanx, span, False if not sharex else None, rc['subplots.span']) # noqa: E501
spany = _not_none(spany, span, False if not sharey else None, rc['subplots.span']) # noqa: E501
if spanx and (alignx or align): # only warn when explicitly requested
warnings._warn_proplot('"alignx" has no effect when spanx=True.')
if spany and (aligny or align):
warnings._warn_proplot('"aligny" has no effect when spany=True.')
self._spanx = spanx
self._spany = spany
self._alignx = _not_none(alignx, align, rc['subplots.align'])
self._aligny = _not_none(aligny, align, rc['subplots.align'])
self._spanx = bool(spanx)
self._spany = bool(spany)
alignx = _not_none(alignx, align, rc['subplots.align'])
aligny = _not_none(aligny, align, rc['subplots.align'])
self._alignx = bool(alignx)
self._aligny = bool(aligny)

# Initialize the figure
# NOTE: Super labels are stored inside {axes: text} dictionaries
Expand Down

0 comments on commit 73f355a

Please sign in to comment.