diff --git a/doc/users/next_whats_new/2020-04-12-spines.rst b/doc/users/next_whats_new/2020-04-12-spines.rst new file mode 100644 index 000000000000..d4105aecb4cb --- /dev/null +++ b/doc/users/next_whats_new/2020-04-12-spines.rst @@ -0,0 +1,19 @@ +``Axes.spines`` +--------------- + +``Axes.spines`` is now a dedicated container class `.Spines` for a set of +`.Spine`\s instead of an ``OrderedDict``. On top of dict-like access, +``Axes.spines`` now also supports some ``pandas.Series``-like features. + +Accessing single elements by item or by attribute + + ax.spines['top'].set_visible(False) + ax.spines.top.set_visible(False) + +Accessing a subset of items:: + + ax.spines[['top', 'right']].set_visible(False) + +Accessing all items simultaneously:: + + ax.spines[:].set_visible(False) diff --git a/examples/axes_grid1/demo_axes_rgb.py b/examples/axes_grid1/demo_axes_rgb.py index 1e1872de962d..f7af129d74be 100644 --- a/examples/axes_grid1/demo_axes_rgb.py +++ b/examples/axes_grid1/demo_axes_rgb.py @@ -58,8 +58,7 @@ def demo_rgb2(): for ax in fig.axes: ax.tick_params(axis='both', direction='in') - for sp1 in ax.spines.values(): - sp1.set_color("w") + ax.spines[:].set_color("w") for tick in ax.xaxis.get_major_ticks() + ax.yaxis.get_major_ticks(): tick.tick1line.set_markeredgecolor("w") tick.tick2line.set_markeredgecolor("w") diff --git a/examples/images_contours_and_fields/image_annotated_heatmap.py b/examples/images_contours_and_fields/image_annotated_heatmap.py index fd912dffa755..76e6f60a51e0 100644 --- a/examples/images_contours_and_fields/image_annotated_heatmap.py +++ b/examples/images_contours_and_fields/image_annotated_heatmap.py @@ -149,8 +149,7 @@ def heatmap(data, row_labels, col_labels, ax=None, rotation_mode="anchor") # Turn spines off and create white grid. - for edge, spine in ax.spines.items(): - spine.set_visible(False) + ax.spines[:].set_visible(False) ax.set_xticks(np.arange(data.shape[1]+1)-.5, minor=True) ax.set_yticks(np.arange(data.shape[0]+1)-.5, minor=True) diff --git a/examples/lines_bars_and_markers/linestyles.py b/examples/lines_bars_and_markers/linestyles.py index af9d28443037..35920617c90c 100644 --- a/examples/lines_bars_and_markers/linestyles.py +++ b/examples/lines_bars_and_markers/linestyles.py @@ -54,8 +54,7 @@ def plot_linestyles(ax, linestyles, title): yticks=np.arange(len(linestyles)), yticklabels=yticklabels) ax.tick_params(left=False, bottom=False, labelbottom=False) - for spine in ax.spines.values(): - spine.set_visible(False) + ax.spines[:].set_visible(False) # For each line style, add a text annotation with a small offset from # the reference point (0 in Axes coords, y tick value in Data coords). diff --git a/examples/lines_bars_and_markers/timeline.py b/examples/lines_bars_and_markers/timeline.py index 3911fcd6559c..ee1d016f8f95 100644 --- a/examples/lines_bars_and_markers/timeline.py +++ b/examples/lines_bars_and_markers/timeline.py @@ -89,8 +89,7 @@ # remove y axis and spines ax.yaxis.set_visible(False) -for spine in ["left", "top", "right"]: - ax.spines[spine].set_visible(False) +ax.spines[["left", "top", "right"]].set_visible(False) ax.margins(y=0.1) plt.show() diff --git a/examples/showcase/bachelors_degrees_by_gender.py b/examples/showcase/bachelors_degrees_by_gender.py index aef1736ce761..079ba399634c 100644 --- a/examples/showcase/bachelors_degrees_by_gender.py +++ b/examples/showcase/bachelors_degrees_by_gender.py @@ -32,10 +32,7 @@ '#17becf', '#9edae5']) # Remove the plot frame lines. They are unnecessary here. -ax.spines['top'].set_visible(False) -ax.spines['bottom'].set_visible(False) -ax.spines['right'].set_visible(False) -ax.spines['left'].set_visible(False) +ax.spines[:].set_visible(False) # Ensure that the axis ticks only show up on the bottom and left of the plot. # Ticks on the right and top of the plot are generally unnecessary. diff --git a/examples/showcase/integral.py b/examples/showcase/integral.py index 06f95d8dec08..e0d07fec3de2 100644 --- a/examples/showcase/integral.py +++ b/examples/showcase/integral.py @@ -42,8 +42,8 @@ def func(x): fig.text(0.9, 0.05, '$x$') fig.text(0.1, 0.9, '$y$') -ax.spines['right'].set_visible(False) -ax.spines['top'].set_visible(False) +ax.spines.right.set_visible(False) +ax.spines.top.set_visible(False) ax.xaxis.set_ticks_position('bottom') ax.set_xticks((a, b)) diff --git a/examples/showcase/xkcd.py b/examples/showcase/xkcd.py index 22fec6cfdc87..4ab16c66a536 100644 --- a/examples/showcase/xkcd.py +++ b/examples/showcase/xkcd.py @@ -16,8 +16,8 @@ fig = plt.figure() ax = fig.add_axes((0.1, 0.2, 0.8, 0.7)) - ax.spines['right'].set_color('none') - ax.spines['top'].set_color('none') + ax.spines.right.set_color('none') + ax.spines.top.set_color('none') ax.set_xticks([]) ax.set_yticks([]) ax.set_ylim([-30, 10]) @@ -47,8 +47,8 @@ fig = plt.figure() ax = fig.add_axes((0.1, 0.2, 0.8, 0.7)) ax.bar([0, 1], [0, 100], 0.25) - ax.spines['right'].set_color('none') - ax.spines['top'].set_color('none') + ax.spines.right.set_color('none') + ax.spines.top.set_color('none') ax.xaxis.set_ticks_position('bottom') ax.set_xticks([0, 1]) ax.set_xticklabels(['CONFIRMED BY\nEXPERIMENT', 'REFUTED BY\nEXPERIMENT']) diff --git a/examples/specialty_plots/skewt.py b/examples/specialty_plots/skewt.py index 50167ffb203c..e6023fef4b5b 100644 --- a/examples/specialty_plots/skewt.py +++ b/examples/specialty_plots/skewt.py @@ -85,11 +85,11 @@ class SkewXAxes(Axes): def _init_axis(self): # Taken from Axes and modified to use our modified X-axis self.xaxis = SkewXAxis(self) - self.spines['top'].register_axis(self.xaxis) - self.spines['bottom'].register_axis(self.xaxis) + self.spines.top.register_axis(self.xaxis) + self.spines.bottom.register_axis(self.xaxis) self.yaxis = maxis.YAxis(self) - self.spines['left'].register_axis(self.yaxis) - self.spines['right'].register_axis(self.yaxis) + self.spines.left.register_axis(self.yaxis) + self.spines.right.register_axis(self.yaxis) def _gen_axes_spines(self): spines = {'top': SkewSpine.linear_spine(self, 'top'), diff --git a/examples/subplots_axes_and_figures/broken_axis.py b/examples/subplots_axes_and_figures/broken_axis.py index 0490768cf0ea..38d859d0e00d 100644 --- a/examples/subplots_axes_and_figures/broken_axis.py +++ b/examples/subplots_axes_and_figures/broken_axis.py @@ -31,8 +31,8 @@ ax2.set_ylim(0, .22) # most of the data # hide the spines between ax and ax2 -ax1.spines['bottom'].set_visible(False) -ax2.spines['top'].set_visible(False) +ax1.spines.bottom.set_visible(False) +ax2.spines.top.set_visible(False) ax1.xaxis.tick_top() ax1.tick_params(labeltop=False) # don't put tick labels at the top ax2.xaxis.tick_bottom() diff --git a/examples/ticks_and_spines/centered_spines_with_arrows.py b/examples/ticks_and_spines/centered_spines_with_arrows.py index a22f613da764..31f01f61fc8d 100644 --- a/examples/ticks_and_spines/centered_spines_with_arrows.py +++ b/examples/ticks_and_spines/centered_spines_with_arrows.py @@ -14,11 +14,9 @@ fig, ax = plt.subplots() # Move the left and bottom spines to x = 0 and y = 0, respectively. -ax.spines["left"].set_position(("data", 0)) -ax.spines["bottom"].set_position(("data", 0)) +ax.spines[["left", "bottom"]].set_position(("data", 0)) # Hide the top and right spines. -ax.spines["top"].set_visible(False) -ax.spines["right"].set_visible(False) +ax.spines[["top", "right"]].set_visible(False) # Draw arrows (as black triangles: ">k"/"^k") at the end of the axes. In each # case, one of the coordinates (0) is a data coordinate (i.e., y = 0 or x = 0, diff --git a/examples/ticks_and_spines/multiple_yaxis_with_spines.py b/examples/ticks_and_spines/multiple_yaxis_with_spines.py index 6cd49f35432f..54eebdae11ba 100644 --- a/examples/ticks_and_spines/multiple_yaxis_with_spines.py +++ b/examples/ticks_and_spines/multiple_yaxis_with_spines.py @@ -24,7 +24,7 @@ # Offset the right spine of twin2. The ticks and label have already been # placed on the right by twinx above. -twin2.spines["right"].set_position(("axes", 1.2)) +twin2.spines.right.set_position(("axes", 1.2)) p1, = ax.plot([0, 1, 2], [0, 1, 2], "b-", label="Density") p2, = twin1.plot([0, 1, 2], [0, 3, 2], "r-", label="Temperature") diff --git a/examples/ticks_and_spines/spine_placement_demo.py b/examples/ticks_and_spines/spine_placement_demo.py index b831209abad3..211c723ea1ae 100644 --- a/examples/ticks_and_spines/spine_placement_demo.py +++ b/examples/ticks_and_spines/spine_placement_demo.py @@ -21,40 +21,40 @@ ax = fig.add_subplot(2, 2, 1) ax.set_title('centered spines') ax.plot(x, y) -ax.spines['left'].set_position('center') -ax.spines['right'].set_color('none') -ax.spines['bottom'].set_position('center') -ax.spines['top'].set_color('none') +ax.spines.left.set_position('center') +ax.spines.right.set_color('none') +ax.spines.bottom.set_position('center') +ax.spines.top.set_color('none') ax.xaxis.set_ticks_position('bottom') ax.yaxis.set_ticks_position('left') ax = fig.add_subplot(2, 2, 2) ax.set_title('zeroed spines') ax.plot(x, y) -ax.spines['left'].set_position('zero') -ax.spines['right'].set_color('none') -ax.spines['bottom'].set_position('zero') -ax.spines['top'].set_color('none') +ax.spines.left.set_position('zero') +ax.spines.right.set_color('none') +ax.spines.bottom.set_position('zero') +ax.spines.top.set_color('none') ax.xaxis.set_ticks_position('bottom') ax.yaxis.set_ticks_position('left') ax = fig.add_subplot(2, 2, 3) ax.set_title('spines at axes (0.6, 0.1)') ax.plot(x, y) -ax.spines['left'].set_position(('axes', 0.6)) -ax.spines['right'].set_color('none') -ax.spines['bottom'].set_position(('axes', 0.1)) -ax.spines['top'].set_color('none') +ax.spines.left.set_position(('axes', 0.6)) +ax.spines.right.set_color('none') +ax.spines.bottom.set_position(('axes', 0.1)) +ax.spines.top.set_color('none') ax.xaxis.set_ticks_position('bottom') ax.yaxis.set_ticks_position('left') ax = fig.add_subplot(2, 2, 4) ax.set_title('spines at data (1, 2)') ax.plot(x, y) -ax.spines['left'].set_position(('data', 1)) -ax.spines['right'].set_color('none') -ax.spines['bottom'].set_position(('data', 2)) -ax.spines['top'].set_color('none') +ax.spines.left.set_position(('data', 1)) +ax.spines.right.set_color('none') +ax.spines.bottom.set_position(('data', 2)) +ax.spines.top.set_color('none') ax.xaxis.set_ticks_position('bottom') ax.yaxis.set_ticks_position('left') diff --git a/examples/ticks_and_spines/spines.py b/examples/ticks_and_spines/spines.py index e0522d78ce74..08e3700d1387 100644 --- a/examples/ticks_and_spines/spines.py +++ b/examples/ticks_and_spines/spines.py @@ -26,8 +26,8 @@ ax1.set_title('bottom-left spines') # Hide the right and top spines -ax1.spines['right'].set_visible(False) -ax1.spines['top'].set_visible(False) +ax1.spines.right.set_visible(False) +ax1.spines.top.set_visible(False) # Only show ticks on the left and bottom spines ax1.yaxis.set_ticks_position('left') ax1.xaxis.set_ticks_position('bottom') @@ -35,10 +35,10 @@ ax2.plot(x, y) # Only draw spine between the y-ticks -ax2.spines['left'].set_bounds(-1, 1) +ax2.spines.left.set_bounds(-1, 1) # Hide the right and top spines -ax2.spines['right'].set_visible(False) -ax2.spines['top'].set_visible(False) +ax2.spines.right.set_visible(False) +ax2.spines.top.set_visible(False) # Only show ticks on the left and bottom spines ax2.yaxis.set_ticks_position('left') ax2.xaxis.set_ticks_position('bottom') diff --git a/examples/ticks_and_spines/spines_bounds.py b/examples/ticks_and_spines/spines_bounds.py index dc40a7bbe962..96205a43d38e 100644 --- a/examples/ticks_and_spines/spines_bounds.py +++ b/examples/ticks_and_spines/spines_bounds.py @@ -27,10 +27,10 @@ ax.set_yticks([-1, 0, 1]) # Only draw spine between the y-ticks -ax.spines['left'].set_bounds((-1, 1)) +ax.spines.left.set_bounds((-1, 1)) # Hide the right and top spines -ax.spines['right'].set_visible(False) -ax.spines['top'].set_visible(False) +ax.spines.right.set_visible(False) +ax.spines.top.set_visible(False) # Only show ticks on the left and bottom spines ax.yaxis.set_ticks_position('left') ax.xaxis.set_ticks_position('bottom') diff --git a/examples/ticks_and_spines/spines_dropped.py b/examples/ticks_and_spines/spines_dropped.py index be9662df8e14..954e1c7ffb3a 100644 --- a/examples/ticks_and_spines/spines_dropped.py +++ b/examples/ticks_and_spines/spines_dropped.py @@ -18,11 +18,11 @@ ax.set_title('dropped spines') # Move left and bottom spines outward by 10 points -ax.spines['left'].set_position(('outward', 10)) -ax.spines['bottom'].set_position(('outward', 10)) +ax.spines.left.set_position(('outward', 10)) +ax.spines.bottom.set_position(('outward', 10)) # Hide the right and top spines -ax.spines['right'].set_visible(False) -ax.spines['top'].set_visible(False) +ax.spines.right.set_visible(False) +ax.spines.top.set_visible(False) # Only show ticks on the left and bottom spines ax.yaxis.set_ticks_position('left') ax.xaxis.set_ticks_position('bottom') diff --git a/examples/ticks_and_spines/tick-formatters.py b/examples/ticks_and_spines/tick-formatters.py index d410c90192cf..3693b51a4b1a 100644 --- a/examples/ticks_and_spines/tick-formatters.py +++ b/examples/ticks_and_spines/tick-formatters.py @@ -17,9 +17,9 @@ def setup(ax, title): """Set up common parameters for the Axes in the example.""" # only show the bottom spine ax.yaxis.set_major_locator(ticker.NullLocator()) - ax.spines['right'].set_color('none') - ax.spines['left'].set_color('none') - ax.spines['top'].set_color('none') + ax.spines.right.set_color('none') + ax.spines.left.set_color('none') + ax.spines.top.set_color('none') # define tick positions ax.xaxis.set_major_locator(ticker.MultipleLocator(1.00)) diff --git a/examples/ticks_and_spines/tick-locators.py b/examples/ticks_and_spines/tick-locators.py index bda576c26ea1..eeac29a07ad2 100644 --- a/examples/ticks_and_spines/tick-locators.py +++ b/examples/ticks_and_spines/tick-locators.py @@ -17,9 +17,9 @@ def setup(ax, title): """Set up common parameters for the Axes in the example.""" # only show the bottom spine ax.yaxis.set_major_locator(ticker.NullLocator()) - ax.spines['right'].set_color('none') - ax.spines['left'].set_color('none') - ax.spines['top'].set_color('none') + ax.spines.right.set_color('none') + ax.spines.left.set_color('none') + ax.spines.top.set_color('none') ax.xaxis.set_ticks_position('bottom') ax.tick_params(which='major', width=1.00, length=5) diff --git a/examples/userdemo/demo_gridspec06.py b/examples/userdemo/demo_gridspec06.py index 80138b3d651c..c42224ce1e7b 100644 --- a/examples/userdemo/demo_gridspec06.py +++ b/examples/userdemo/demo_gridspec06.py @@ -30,9 +30,9 @@ def squiggle_xy(a, b, c, d): # show only the outside spines for ax in fig.get_axes(): ss = ax.get_subplotspec() - ax.spines['top'].set_visible(ss.is_first_row()) - ax.spines['bottom'].set_visible(ss.is_last_row()) - ax.spines['left'].set_visible(ss.is_first_col()) - ax.spines['right'].set_visible(ss.is_last_col()) + ax.spines.top.set_visible(ss.is_first_row()) + ax.spines.bottom.set_visible(ss.is_last_row()) + ax.spines.left.set_visible(ss.is_first_col()) + ax.spines.right.set_visible(ss.is_last_col()) plt.show() diff --git a/lib/matplotlib/axes/_base.py b/lib/matplotlib/axes/_base.py index b9f0498045f6..a0f5498726df 100644 --- a/lib/matplotlib/axes/_base.py +++ b/lib/matplotlib/axes/_base.py @@ -535,7 +535,7 @@ def __init__(self, fig, rect, # placeholder for any colorbars added that use this axes. # (see colorbar.py): self._colorbars = [] - self.spines = self._gen_axes_spines() + self.spines = mspines.Spines.from_dict(self._gen_axes_spines()) # this call may differ for non-sep axes, e.g., polar self._init_axis() @@ -660,11 +660,11 @@ def get_window_extent(self, *args, **kwargs): def _init_axis(self): # This is moved out of __init__ because non-separable axes don't use it self.xaxis = maxis.XAxis(self) - self.spines['bottom'].register_axis(self.xaxis) - self.spines['top'].register_axis(self.xaxis) + self.spines.bottom.register_axis(self.xaxis) + self.spines.top.register_axis(self.xaxis) self.yaxis = maxis.YAxis(self) - self.spines['left'].register_axis(self.yaxis) - self.spines['right'].register_axis(self.yaxis) + self.spines.left.register_axis(self.yaxis) + self.spines.right.register_axis(self.yaxis) self._update_transScale() def set_figure(self, fig): @@ -763,10 +763,10 @@ def get_xaxis_transform(self, which='grid'): return self._xaxis_transform elif which == 'tick1': # for cartesian projection, this is bottom spine - return self.spines['bottom'].get_spine_transform() + return self.spines.bottom.get_spine_transform() elif which == 'tick2': # for cartesian projection, this is top spine - return self.spines['top'].get_spine_transform() + return self.spines.top.get_spine_transform() else: raise ValueError('unknown value for which') @@ -839,10 +839,10 @@ def get_yaxis_transform(self, which='grid'): return self._yaxis_transform elif which == 'tick1': # for cartesian projection, this is bottom spine - return self.spines['left'].get_spine_transform() + return self.spines.left.get_spine_transform() elif which == 'tick2': # for cartesian projection, this is top spine - return self.spines['right'].get_spine_transform() + return self.spines.right.get_spine_transform() else: raise ValueError('unknown value for which') diff --git a/lib/matplotlib/axes/_secondary_axes.py b/lib/matplotlib/axes/_secondary_axes.py index a3a88357426b..58db44ad05c9 100644 --- a/lib/matplotlib/axes/_secondary_axes.py +++ b/lib/matplotlib/axes/_secondary_axes.py @@ -49,10 +49,8 @@ def __init__(self, parent, orientation, location, functions, **kwargs): otheraxis.set_major_locator(mticker.NullLocator()) otheraxis.set_ticks_position('none') - for st in self._otherstrings: - self.spines[st].set_visible(False) - for st in self._locstrings: - self.spines[st].set_visible(True) + self.spines[self._otherstrings].set_visible(False) + self.spines[self._locstrings].set_visible(True) if self._pos < 0.5: # flip the location strings... @@ -259,13 +257,13 @@ def set_color(self, color): """ if self._orientation == 'x': self.tick_params(axis='x', colors=color) - self.spines['bottom'].set_color(color) - self.spines['top'].set_color(color) + self.spines.bottom.set_color(color) + self.spines.top.set_color(color) self.xaxis.label.set_color(color) else: self.tick_params(axis='y', colors=color) - self.spines['left'].set_color(color) - self.spines['right'].set_color(color) + self.spines.left.set_color(color) + self.spines.right.set_color(color) self.yaxis.label.set_color(color) diff --git a/lib/matplotlib/spines.py b/lib/matplotlib/spines.py index dcd259a95bf0..86957c6ddea1 100644 --- a/lib/matplotlib/spines.py +++ b/lib/matplotlib/spines.py @@ -1,3 +1,6 @@ +from collections.abc import MutableMapping +import functools + import numpy as np import matplotlib @@ -534,3 +537,114 @@ def set_color(self, c): """ self.set_edgecolor(c) self.stale = True + + +class SpinesProxy: + """ + A proxy to broadcast ``set_*`` method calls to all contained `.Spines`. + + The proxy cannot be used for any other operations on its members. + + The supported methods are determined dynamically based on the contained + spines. If not all spines support a given method, it's executed only on + the subset of spines that support it. + """ + def __init__(self, spine_dict): + self._spine_dict = spine_dict + + def __getattr__(self, name): + broadcast_targets = [spine for spine in self._spine_dict.values() + if hasattr(spine, name)] + if not name.startswith('set_') or not broadcast_targets: + raise AttributeError( + f"'SpinesProxy' object has no attribute '{name}'") + + def x(_targets, _funcname, *args, **kwargs): + for spine in _targets: + getattr(spine, _funcname)(*args, **kwargs) + x = functools.partial(x, broadcast_targets, name) + x.__doc__ = broadcast_targets[0].__doc__ + return x + + def __dir__(self): + names = [] + for spine in self._spine_dict.values(): + names.extend(name + for name in dir(spine) if name.startswith('set_')) + return list(sorted(set(names))) + + +class Spines(MutableMapping): + r""" + The container of all `.Spine`\s in an Axes. + + The interface is dict-like mapping names (e.g. 'left') to `.Spine` objects. + Additionally it implements some pandas.Series-like features like accessing + elements by attribute:: + + spines['top'].set_visible(False) + spines.top.set_visible(False) + + Multiple spines can be addressed simultaneously by passing a list:: + + spines[['top', 'right']].set_visible(False) + + Use an open slice to address all spines:: + + spines[:].set_visible(False) + + The latter two indexing methods will return a `SpinesProxy` that broadcasts + all ``set_*`` calls to its members, but cannot be used for any other + operation. + """ + def __init__(self, **kwargs): + self._dict = kwargs + + @classmethod + def from_dict(cls, d): + return cls(**d) + + def __getstate__(self): + return self._dict + + def __setstate__(self, state): + self.__init__(**state) + + def __getattr__(self, name): + try: + return self._dict[name] + except KeyError: + raise ValueError( + f"'Spines' object does not contain a '{name}' spine") + + def __getitem__(self, key): + if isinstance(key, list): + unknown_keys = [k for k in key if k not in self._dict] + if unknown_keys: + raise KeyError(', '.join(unknown_keys)) + return SpinesProxy({k: v for k, v in self._dict.items() + if k in key}) + if isinstance(key, tuple): + raise ValueError('Multiple spines must be passed as a single list') + if isinstance(key, slice): + if key.start is None and key.stop is None and key.step is None: + return SpinesProxy(self._dict) + else: + raise ValueError( + 'Spines does not support slicing except for the fully ' + 'open slice [:] to access all spines.') + return self._dict[key] + + def __setitem__(self, key, value): + # TODO: Do we want to deprecate adding spines? + self._dict[key] = value + + def __delitem__(self, key): + # TODO: Do we want to deprecate deleting spines? + del self._dict[key] + + def __iter__(self): + return iter(self._dict) + + def __len__(self): + return len(self._dict) diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py index 6d3050c3df8e..eb977049a179 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -4506,8 +4506,7 @@ def test_twin_spines(): def make_patch_spines_invisible(ax): ax.set_frame_on(True) ax.patch.set_visible(False) - for sp in ax.spines.values(): - sp.set_visible(False) + ax.spines[:].set_visible(False) fig = plt.figure(figsize=(4, 3)) fig.subplots_adjust(right=0.75) @@ -4518,13 +4517,13 @@ def make_patch_spines_invisible(ax): # Offset the right spine of par2. The ticks and label have already been # placed on the right by twinx above. - par2.spines["right"].set_position(("axes", 1.2)) + par2.spines.right.set_position(("axes", 1.2)) # Having been created by twinx, par2 has its frame off, so the line of # its detached spine is invisible. First, activate the frame but make # the patch and spines invisible. make_patch_spines_invisible(par2) # Second, show the right spine. - par2.spines["right"].set_visible(True) + par2.spines.right.set_visible(True) p1, = host.plot([0, 1, 2], [0, 1, 2], "b-") p2, = par1.plot([0, 1, 2], [0, 3, 2], "r-") @@ -5733,8 +5732,7 @@ def test_axisbelow(): ax.grid(color='c', linestyle='-', linewidth=3) ax.tick_params(top=False, bottom=False, left=False, right=False) - for spine in ax.spines.values(): - spine.set_visible(False) + ax.spines[:].set_visible(False) ax.set_axisbelow(setting) @@ -6124,11 +6122,11 @@ def test_set_position(): def test_spines_properbbox_after_zoom(): fig, ax = plt.subplots() - bb = ax.spines['bottom'].get_window_extent(fig.canvas.get_renderer()) + bb = ax.spines.bottom.get_window_extent(fig.canvas.get_renderer()) # this is what zoom calls: ax._set_view_from_bbox((320, 320, 500, 500), 'in', None, False, False) - bb2 = ax.spines['bottom'].get_window_extent(fig.canvas.get_renderer()) + bb2 = ax.spines.bottom.get_window_extent(fig.canvas.get_renderer()) np.testing.assert_allclose(bb.get_points(), bb2.get_points(), rtol=1e-6) @@ -6386,7 +6384,7 @@ def test_displaced_spine(): with rc_context({'_internal.classic_mode': False}): fig, ax = plt.subplots(dpi=200, figsize=(6, 6)) ax.set(xticklabels=[], yticklabels=[]) - ax.spines['bottom'].set_position(('axes', -0.1)) + ax.spines.bottom.set_position(('axes', -0.1)) fig.canvas.draw() bbaxis, bbspines, bbax, bbtb = color_boxes(fig, ax) diff --git a/lib/matplotlib/tests/test_image.py b/lib/matplotlib/tests/test_image.py index 1fea19fbb8ba..0637f66e4f34 100644 --- a/lib/matplotlib/tests/test_image.py +++ b/lib/matplotlib/tests/test_image.py @@ -522,8 +522,7 @@ def test_rasterize_dpi(): for ax in axs: ax.set_xticks([]) ax.set_yticks([]) - for spine in ax.spines.values(): - spine.set_visible(False) + ax.spines[:].set_visible(False) rcParams['savefig.dpi'] = 10 diff --git a/lib/matplotlib/tests/test_mathtext.py b/lib/matplotlib/tests/test_mathtext.py index faea69dc6c4a..ebfe070995c8 100644 --- a/lib/matplotlib/tests/test_mathtext.py +++ b/lib/matplotlib/tests/test_mathtext.py @@ -266,8 +266,7 @@ def test_mathtext_exceptions(math, msg): def test_single_minus_sign(): plt.figure(figsize=(0.3, 0.3)) plt.text(0.5, 0.5, '$-$') - for spine in plt.gca().spines.values(): - spine.set_visible(False) + plt.gca().spines[:].set_visible(False) plt.gca().set_xticks([]) plt.gca().set_yticks([]) diff --git a/lib/matplotlib/tests/test_skew.py b/lib/matplotlib/tests/test_skew.py index 7ac1c049f8cd..39dc37347bb4 100644 --- a/lib/matplotlib/tests/test_skew.py +++ b/lib/matplotlib/tests/test_skew.py @@ -75,11 +75,11 @@ class SkewXAxes(Axes): def _init_axis(self): # Taken from Axes and modified to use our modified X-axis self.xaxis = SkewXAxis(self) - self.spines['top'].register_axis(self.xaxis) - self.spines['bottom'].register_axis(self.xaxis) + self.spines.top.register_axis(self.xaxis) + self.spines.bottom.register_axis(self.xaxis) self.yaxis = maxis.YAxis(self) - self.spines['left'].register_axis(self.yaxis) - self.spines['right'].register_axis(self.yaxis) + self.spines.left.register_axis(self.yaxis) + self.spines.right.register_axis(self.yaxis) def _gen_axes_spines(self): spines = {'top': SkewSpine.linear_spine(self, 'top'), diff --git a/lib/matplotlib/tests/test_spines.py b/lib/matplotlib/tests/test_spines.py index 5e9ec881ac9f..589badc310d9 100644 --- a/lib/matplotlib/tests/test_spines.py +++ b/lib/matplotlib/tests/test_spines.py @@ -1,9 +1,52 @@ import numpy as np +import pytest import matplotlib.pyplot as plt +from matplotlib.spines import Spines from matplotlib.testing.decorators import check_figures_equal, image_comparison +def test_spine_class(): + """Test Spines and SpinesProxy in isolation.""" + class SpineMock: + def __init__(self): + self.val = None + + def set_val(self, val): + self.val = val + + spines_dict = { + 'left': SpineMock(), + 'right': SpineMock(), + 'top': SpineMock(), + 'bottom': SpineMock(), + } + spines = Spines(**spines_dict) + + assert spines['left'] is spines_dict['left'] + assert spines.left is spines_dict['left'] + + spines[['left', 'right']].set_val('x') + assert spines.left.val == 'x' + assert spines.right.val == 'x' + assert spines.top.val is None + assert spines.bottom.val is None + + spines[:].set_val('y') + assert all(spine.val == 'y' for spine in spines.values()) + + with pytest.raises(KeyError, match='foo'): + spines['foo'] + with pytest.raises(KeyError, match='foo, bar'): + spines[['left', 'foo', 'right', 'bar']] + with pytest.raises(ValueError, match='single list'): + spines['left', 'right'] + with pytest.raises(ValueError, match='Spines does not support slicing'): + spines['left':'right'] + with pytest.raises(ValueError, match='Spines does not support slicing'): + spines['top':] + + @image_comparison(['spines_axes_positions']) def test_spines_axes_positions(): # SF bug 2852168 @@ -13,21 +56,21 @@ def test_spines_axes_positions(): ax = fig.add_subplot(1, 1, 1) ax.set_title('centered spines') ax.plot(x, y) - ax.spines['right'].set_position(('axes', 0.1)) + ax.spines.right.set_position(('axes', 0.1)) ax.yaxis.set_ticks_position('right') - ax.spines['top'].set_position(('axes', 0.25)) + ax.spines.top.set_position(('axes', 0.25)) ax.xaxis.set_ticks_position('top') - ax.spines['left'].set_color('none') - ax.spines['bottom'].set_color('none') + ax.spines.left.set_color('none') + ax.spines.bottom.set_color('none') @image_comparison(['spines_data_positions']) def test_spines_data_positions(): fig, ax = plt.subplots() - ax.spines['left'].set_position(('data', -1.5)) - ax.spines['top'].set_position(('data', 0.5)) - ax.spines['right'].set_position(('data', -0.5)) - ax.spines['bottom'].set_position('zero') + ax.spines.left.set_position(('data', -1.5)) + ax.spines.top.set_position(('data', 0.5)) + ax.spines.right.set_position(('data', -0.5)) + ax.spines.bottom.set_position('zero') ax.set_xlim([-2, 2]) ax.set_ylim([-2, 2]) @@ -42,14 +85,14 @@ def test_spine_nonlinear_data_positions(fig_test, fig_ref): # linewidth to distinguish them. The calls to tick_params removes labels # (for image comparison purposes) and harmonizes tick positions with the # reference). - ax.spines["left"].set_position(("data", 1)) - ax.spines["left"].set_linewidth(2) - ax.spines["right"].set_position(("data", .1)) + ax.spines.left.set_position(("data", 1)) + ax.spines.left.set_linewidth(2) + ax.spines.right.set_position(("data", .1)) ax.tick_params(axis="y", labelleft=False, direction="in") ax = fig_ref.add_subplot() ax.set(xscale="log", xlim=(.1, 1)) - ax.spines["right"].set_linewidth(2) + ax.spines.right.set_linewidth(2) ax.tick_params(axis="y", labelleft=False, left=False, right=True) @@ -67,24 +110,24 @@ def test_label_without_ticks(): plt.subplots_adjust(left=0.3, bottom=0.3) ax.plot(np.arange(10)) ax.yaxis.set_ticks_position('left') - ax.spines['left'].set_position(('outward', 30)) - ax.spines['right'].set_visible(False) + ax.spines.left.set_position(('outward', 30)) + ax.spines.right.set_visible(False) ax.set_ylabel('y label') ax.xaxis.set_ticks_position('bottom') - ax.spines['bottom'].set_position(('outward', 30)) - ax.spines['top'].set_visible(False) + ax.spines.bottom.set_position(('outward', 30)) + ax.spines.top.set_visible(False) ax.set_xlabel('x label') ax.xaxis.set_ticks([]) ax.yaxis.set_ticks([]) plt.draw() - spine = ax.spines['left'] + spine = ax.spines.left spinebbox = spine.get_transform().transform_path( spine.get_path()).get_extents() assert ax.yaxis.label.get_position()[0] < spinebbox.xmin, \ "Y-Axis label not left of the spine" - spine = ax.spines['bottom'] + spine = ax.spines.bottom spinebbox = spine.get_transform().transform_path( spine.get_path()).get_extents() assert ax.xaxis.label.get_position()[1] < spinebbox.ymin, \ diff --git a/lib/mpl_toolkits/axisartist/axislines.py b/lib/mpl_toolkits/axisartist/axislines.py index 3e2dca2c573b..9d8e9f864f72 100644 --- a/lib/mpl_toolkits/axisartist/axislines.py +++ b/lib/mpl_toolkits/axisartist/axislines.py @@ -467,14 +467,12 @@ def toggle_axisline(self, b=None): b = not self._axisline_on if b: self._axisline_on = True - for s in self.spines.values(): - s.set_visible(False) + self.spines[:].set_visible(False) self.xaxis.set_visible(False) self.yaxis.set_visible(False) else: self._axisline_on = False - for s in self.spines.values(): - s.set_visible(True) + self.spines[:].set_visible(True) self.xaxis.set_visible(True) self.yaxis.set_visible(True) diff --git a/lib/mpl_toolkits/mplot3d/axes3d.py b/lib/mpl_toolkits/mplot3d/axes3d.py index 3d08a12b2ffc..e981c5f62a85 100644 --- a/lib/mpl_toolkits/mplot3d/axes3d.py +++ b/lib/mpl_toolkits/mplot3d/axes3d.py @@ -133,8 +133,7 @@ def __init__( # mplot3d currently manages its own spines and needs these turned off # for bounding box calculations - for k in self.spines.keys(): - self.spines[k].set_visible(False) + self.spines[:].set_visible(False) def set_axis_off(self): self._axis3don = False diff --git a/tutorials/intermediate/gridspec.py b/tutorials/intermediate/gridspec.py index bc19a618c8d9..4debe8a9d43a 100644 --- a/tutorials/intermediate/gridspec.py +++ b/tutorials/intermediate/gridspec.py @@ -247,10 +247,10 @@ def squiggle_xy(a, b, c, d, i=np.arange(0.0, 2*np.pi, 0.05)): # show only the outside spines for ax in fig11.get_axes(): ss = ax.get_subplotspec() - ax.spines['top'].set_visible(ss.is_first_row()) - ax.spines['bottom'].set_visible(ss.is_last_row()) - ax.spines['left'].set_visible(ss.is_first_col()) - ax.spines['right'].set_visible(ss.is_last_col()) + ax.spines.top.set_visible(ss.is_first_row()) + ax.spines.bottom.set_visible(ss.is_last_row()) + ax.spines.left.set_visible(ss.is_first_col()) + ax.spines.right.set_visible(ss.is_last_col()) plt.show()