From 1e7f0601ed9b79ef795ec41ebb2ec19fd8aac5bb Mon Sep 17 00:00:00 2001 From: Jody Klymak Date: Mon, 24 Sep 2018 10:56:04 -0700 Subject: [PATCH 1/3] ENH: add secondary x/y axis --- .flake8 | 1 + doc/api/axes_api.rst | 2 + doc/users/next_whats_new/2018-09-08-JMK.rst | 16 + .../secondary_axis.py | 179 +++++++ lib/matplotlib/_constrained_layout.py | 11 +- lib/matplotlib/axes/_axes.py | 75 +++ lib/matplotlib/axes/_base.py | 18 +- lib/matplotlib/axes/_secondary_axes.py | 463 ++++++++++++++++++ lib/matplotlib/axis.py | 6 +- lib/matplotlib/dates.py | 1 - lib/matplotlib/scale.py | 42 ++ .../test_axes/secondary_xy.png | Bin 0 -> 42332 bytes lib/matplotlib/tests/test_axes.py | 33 ++ lib/mpl_toolkits/mplot3d/axes3d.py | 36 +- 14 files changed, 861 insertions(+), 22 deletions(-) create mode 100644 doc/users/next_whats_new/2018-09-08-JMK.rst create mode 100644 examples/subplots_axes_and_figures/secondary_axis.py create mode 100644 lib/matplotlib/axes/_secondary_axes.py create mode 100644 lib/matplotlib/tests/baseline_images/test_axes/secondary_xy.png diff --git a/.flake8 b/.flake8 index c7e9114821ff..007f636b27df 100644 --- a/.flake8 +++ b/.flake8 @@ -228,6 +228,7 @@ per-file-ignores = examples/subplots_axes_and_figures/axes_zoom_effect.py: E402 examples/subplots_axes_and_figures/demo_constrained_layout.py: E402 examples/subplots_axes_and_figures/demo_tight_layout.py: E402 + examples/subplots_axes_and_figures/secondary_axis.py: E402 examples/subplots_axes_and_figures/two_scales.py: E402 examples/subplots_axes_and_figures/zoom_inset_axes.py: E402 examples/tests/backend_driver_sgskip.py: E402, E501 diff --git a/doc/api/axes_api.rst b/doc/api/axes_api.rst index 3e2f7cdd8c34..a3660fac3ba7 100644 --- a/doc/api/axes_api.rst +++ b/doc/api/axes_api.rst @@ -188,6 +188,8 @@ Text and Annotations Axes.inset_axes Axes.indicate_inset Axes.indicate_inset_zoom + Axes.secondary_xaxis + Axes.secondary_yaxis Fields diff --git a/doc/users/next_whats_new/2018-09-08-JMK.rst b/doc/users/next_whats_new/2018-09-08-JMK.rst new file mode 100644 index 000000000000..1c3bc954ae7f --- /dev/null +++ b/doc/users/next_whats_new/2018-09-08-JMK.rst @@ -0,0 +1,16 @@ +:orphan: + +Secondary x/y Axis support +-------------------------- + +A new method provides the ability to add a second axis to an existing +axes via `.Axes.secondary_xaxis` and `.Axes.secondary_yaxis`. See +:doc:`/gallery/subplots_axes_and_figures/secondary_axis` for examples. + +.. plot:: + + import matplotlib.pyplot as plt + + fig, ax = plt.subplots(figsize=(5, 3)) + ax.plot(range(360)) + ax.secondary_xaxis('top', functions=(np.deg2rad, np.rad2deg)) diff --git a/examples/subplots_axes_and_figures/secondary_axis.py b/examples/subplots_axes_and_figures/secondary_axis.py new file mode 100644 index 000000000000..466b1a2f8e32 --- /dev/null +++ b/examples/subplots_axes_and_figures/secondary_axis.py @@ -0,0 +1,179 @@ +""" +============== +Secondary Axis +============== + +Sometimes we want as secondary axis on a plot, for instance to convert +radians to degrees on the same plot. We can do this by making a child +axes with only one axis visible via `.Axes.axes.secondary_xaxis` and +`.Axes.axes.secondary_yaxis`. + +If we want to label the top of the axes: +""" + +import matplotlib.pyplot as plt +import numpy as np +import datetime +import matplotlib.dates as mdates +from matplotlib.transforms import Transform +from matplotlib.ticker import ( + AutoLocator, AutoMinorLocator) + +fig, ax = plt.subplots(constrained_layout=True) +x = np.arange(0, 360, 1) +y = np.sin(2 * x * np.pi / 180) +ax.plot(x, y) +ax.set_xlabel('angle [degrees]') +ax.set_ylabel('signal') +ax.set_title('Sine wave') +secax = ax.secondary_xaxis('top') +plt.show() + +########################################################################### +# However, its often useful to label the secondary axis with something +# other than the labels in the main axis. In that case we need to provide +# both a forward and an inverse conversion function in a tuple +# to the ``functions`` kwarg: + +fig, ax = plt.subplots(constrained_layout=True) +x = np.arange(0, 360, 1) +y = np.sin(2 * x * np.pi / 180) +ax.plot(x, y) +ax.set_xlabel('angle [degrees]') +ax.set_ylabel('signal') +ax.set_title('Sine wave') + + +def deg2rad(x): + return x * np.pi / 180 + + +def rad2deg(x): + return x * 180 / np.pi + +secax = ax.secondary_xaxis('top', functions=(deg2rad, rad2deg)) +secax.set_xlabel('angle [rad]') +plt.show() + +########################################################################### +# Here is the case of converting from wavenumber to wavelength in a +# log-log scale. +# +# .. note :: +# +# In this case, the xscale of the parent is logarithmic, so the child is +# made logarithmic as well. + +fig, ax = plt.subplots(constrained_layout=True) +x = np.arange(0.02, 1, 0.02) +np.random.seed(19680801) +y = np.random.randn(len(x)) ** 2 +ax.loglog(x, y) +ax.set_xlabel('f [Hz]') +ax.set_ylabel('PSD') +ax.set_title('Random spectrum') + + +def forward(x): + return 1 / x + + +def inverse(x): + return 1 / x + +secax = ax.secondary_xaxis('top', functions=(forward, inverse)) +secax.set_xlabel('period [s]') +plt.show() + +########################################################################### +# Sometime we want to relate the axes in a transform that is ad-hoc from +# the data, and is derived empirically. In that case we can set the +# forward and inverse transforms functions to be linear interpolations from the +# one data set to the other. + +fig, ax = plt.subplots(constrained_layout=True) +xdata = np.arange(1, 11, 0.4) +ydata = np.random.randn(len(xdata)) +ax.plot(xdata, ydata, label='Plotted data') + +xold = np.arange(0, 11, 0.2) +# fake data set relating x co-ordinate to another data-derived co-ordinate. +xnew = np.sort(10 * np.exp(-xold / 4) + np.random.randn(len(xold)) / 3) + +ax.plot(xold[3:], xnew[3:], label='Transform data') +ax.set_xlabel('X [m]') +ax.legend() + + +def forward(x): + return np.interp(x, xold, xnew) + + +def inverse(x): + return np.interp(x, xnew, xold) + +secax = ax.secondary_xaxis('top', functions=(forward, inverse)) +secax.xaxis.set_minor_locator(AutoMinorLocator()) +secax.set_xlabel('$X_{other}$') + +plt.show() + +########################################################################### +# A final example translates np.datetime64 to yearday on the x axis and +# from Celsius to Farenheit on the y axis: + + +dates = [datetime.datetime(2018, 1, 1) + datetime.timedelta(hours=k * 6) + for k in range(240)] +temperature = np.random.randn(len(dates)) +fig, ax = plt.subplots(constrained_layout=True) + +ax.plot(dates, temperature) +ax.set_ylabel(r'$T\ [^oC]$') +plt.xticks(rotation=70) + + +def date2yday(x): + """ + x is in matplotlib datenums, so they are floats. + """ + y = x - mdates.date2num(datetime.datetime(2018, 1, 1)) + return y + + +def yday2date(x): + """ + return a matplotlib datenum (x is days since start of year) + """ + y = x + mdates.date2num(datetime.datetime(2018, 1, 1)) + return y + +secaxx = ax.secondary_xaxis('top', functions=(date2yday, yday2date)) +secaxx.set_xlabel('yday [2018]') + + +def CtoF(x): + return x * 1.8 + 32 + + +def FtoC(x): + return (x - 32) / 1.8 + +secaxy = ax.secondary_yaxis('right', functions=(CtoF, FtoC)) +secaxy.set_ylabel(r'$T\ [^oF]$') + +plt.show() + +############################################################################# +# +# ------------ +# +# References +# """""""""" +# +# The use of the following functions and methods is shown in this example: + +import matplotlib + +matplotlib.axes.Axes.secondary_xaxis +matplotlib.axes.Axes.secondary_yaxis diff --git a/lib/matplotlib/_constrained_layout.py b/lib/matplotlib/_constrained_layout.py index a72b79ca7ad1..39795b544a3c 100644 --- a/lib/matplotlib/_constrained_layout.py +++ b/lib/matplotlib/_constrained_layout.py @@ -180,7 +180,8 @@ def do_constrained_layout(fig, renderer, h_pad, w_pad, sup = fig._suptitle bbox = invTransFig(sup.get_window_extent(renderer=renderer)) height = bbox.y1 - bbox.y0 - sup._layoutbox.edit_height(height+h_pad) + if np.isfinite(height): + sup._layoutbox.edit_height(height+h_pad) # OK, the above lines up ax._poslayoutbox with ax._layoutbox # now we need to @@ -266,10 +267,14 @@ def _make_layout_margins(ax, renderer, h_pad, w_pad): """ fig = ax.figure invTransFig = fig.transFigure.inverted().transform_bbox - pos = ax.get_position(original=True) tightbbox = ax.get_tightbbox(renderer=renderer) bbox = invTransFig(tightbbox) + # this can go wrong: + if not (np.isfinite(bbox.width) and np.isfinite(bbox.height)): + # just abort, this is likely a bad set of co-ordinates that + # is transitory... + return # use stored h_pad if it exists h_padt = ax._poslayoutbox.h_pad if h_padt is None: @@ -287,6 +292,8 @@ def _make_layout_margins(ax, renderer, h_pad, w_pad): _log.debug('left %f', (-bbox.x0 + pos.x0 + w_pad)) _log.debug('right %f', (bbox.x1 - pos.x1 + w_pad)) _log.debug('bottom %f', (-bbox.y0 + pos.y0 + h_padt)) + _log.debug('bbox.y0 %f', bbox.y0) + _log.debug('pos.y0 %f', pos.y0) # Sometimes its possible for the solver to collapse # rather than expand axes, so they all have zero height # or width. This stops that... It *should* have been diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index 57f4cc6ca991..13e8cda4e622 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -34,6 +34,7 @@ import matplotlib.tri as mtri from matplotlib.container import BarContainer, ErrorbarContainer, StemContainer from matplotlib.axes._base import _AxesBase, _process_plot_format +from matplotlib.axes._secondary_axes import SecondaryAxis _log = logging.getLogger(__name__) @@ -599,6 +600,80 @@ def indicate_inset_zoom(self, inset_ax, **kwargs): return rectpatch, connects + @docstring.dedent_interpd + def secondary_xaxis(self, location, *, functions=None, **kwargs): + """ + Add a second x-axis to this axes. + + For example if we want to have a second scale for the data plotted on + the xaxis. + + %(_secax_docstring)s + + Examples + -------- + + Add a secondary axes that shows both wavelength for the main + axes that shows wavenumber. + + .. plot:: + + fig, ax = plt.subplots() + ax.loglog(range(1, 360, 5), range(1, 360, 5)) + ax.set_xlabel('frequency [Hz]') + + + def invert(x): + return 1 / x + + secax = ax.secondary_xaxis('top', functions=(invert, invert)) + secax.set_xlabel('Period [s]') + plt.show() + + + """ + if (location in ['top', 'bottom'] or isinstance(location, Number)): + secondary_ax = SecondaryAxis(self, 'x', location, functions, + **kwargs) + self.add_child_axes(secondary_ax) + return secondary_ax + else: + raise ValueError('secondary_xaxis location must be either ' + 'a float or "top"/"bottom"') + + def secondary_yaxis(self, location, *, functions=None, **kwargs): + """ + Add a second y-axis to this axes. + + For example if we want to have a second scale for the data plotted on + the yaxis. + + %(_secax_docstring)s + + Examples + -------- + + Add a secondary axes that converts from radians to degrees + + .. plot:: + + fig, ax = plt.subplots() + ax.plot(range(1, 360, 5), range(1, 360, 5)) + ax.set_ylabel('degrees') + secax = ax.secondary_yaxis('right', functions=(np.deg2rad, + np.rad2deg)) + secax.set_ylabel('radians') + + """ + if location in ['left', 'right'] or isinstance(location, Number): + secondary_ax = SecondaryAxis(self, 'y', location, + functions, **kwargs) + self.add_child_axes(secondary_ax) + return secondary_ax + else: + raise ValueError('secondary_yaxis location must be either ' + 'a float or "left"/"right"') + def text(self, x, y, s, fontdict=None, withdash=False, **kwargs): """ Add text to the axes. diff --git a/lib/matplotlib/axes/_base.py b/lib/matplotlib/axes/_base.py index 2e8359b4288b..868a459538e5 100644 --- a/lib/matplotlib/axes/_base.py +++ b/lib/matplotlib/axes/_base.py @@ -2500,8 +2500,17 @@ def _update_title_position(self, renderer): title.set_position((x, 1.0)) # need to check all our twins too... axs = self._twinned_axes.get_siblings(self) - - top = 0 # the top of all the axes twinned with this axes... + # and all the children + for ax in self.child_axes: + if ax is not None: + locator = ax.get_axes_locator() + if locator: + pos = locator(self, renderer) + ax.apply_aspect(pos) + else: + ax.apply_aspect() + axs = axs + [ax] + top = 0 for ax in axs: try: if (ax.xaxis.get_label_position() == 'top' @@ -2544,6 +2553,8 @@ def draw(self, renderer=None, inframe=False): # prevent triggering call backs during the draw process self._stale = True + + # loop over self and child axes... locator = self.get_axes_locator() if locator: pos = locator(self, renderer) @@ -4315,6 +4326,9 @@ def get_tightbbox(self, renderer, call_axes_locator=True, if bb_yaxis: bb.append(bb_yaxis) + self._update_title_position(renderer) + bb.append(self.get_window_extent(renderer)) + self._update_title_position(renderer) if self.title.get_visible(): bb.append(self.title.get_window_extent(renderer)) diff --git a/lib/matplotlib/axes/_secondary_axes.py b/lib/matplotlib/axes/_secondary_axes.py new file mode 100644 index 000000000000..83960601c260 --- /dev/null +++ b/lib/matplotlib/axes/_secondary_axes.py @@ -0,0 +1,463 @@ +import collections +import numpy as np +import numbers + +import warnings + +import matplotlib.docstring as docstring +import matplotlib.ticker as mticker +import matplotlib.transforms as mtransforms +import matplotlib.scale as mscale +import matplotlib.cbook as cbook + +from matplotlib.axes._base import _AxesBase + +from matplotlib.ticker import ( + AutoLocator, + AutoMinorLocator, + FixedLocator, + FuncFormatter, + LogFormatterSciNotation, + LogLocator, + NullLocator, + NullFormatter, + ScalarFormatter +) + +from matplotlib.scale import Log10Transform + + +def _make_secondary_locator(rect, parent): + """ + Helper function to locate the secondary axes. + + A locator gets used in `Axes.set_aspect` to override the default + locations... It is a function that takes an axes object and + a renderer and tells `set_aspect` where it is to be placed. + + This locator make the transform be in axes-relative co-coordinates + because that is how we specify the "location" of the secondary axes. + + Here *rect* is a rectangle [l, b, w, h] that specifies the + location for the axes in the transform given by *trans* on the + *parent*. + """ + _rect = mtransforms.Bbox.from_bounds(*rect) + bb = mtransforms.TransformedBbox(_rect, parent.transAxes) + tr = parent.figure.transFigure.inverted() + bb = mtransforms.TransformedBbox(bb, tr) + + def secondary_locator(ax, renderer): + return bb + + return secondary_locator + + +class SecondaryAxis(_AxesBase): + """ + General class to hold a Secondary_X/Yaxis. + """ + + def __init__(self, parent, orientation, + location, functions, **kwargs): + """ + See `.secondary_xaxis` and `.secondary_yaxis` for the doc string. + While there is no need for this to be private, it should really be + called by those higher level functions. + """ + + self._functions = functions + self._parent = parent + self._orientation = orientation + self._ticks_set = False + + if self._orientation == 'x': + super().__init__(self._parent.figure, [0, 1., 1, 0.0001], **kwargs) + self._axis = self.xaxis + self._locstrings = ['top', 'bottom'] + self._otherstrings = ['left', 'right'] + elif self._orientation == 'y': + super().__init__(self._parent.figure, [0, 1., 0.0001, 1], **kwargs) + self._axis = self.yaxis + self._locstrings = ['right', 'left'] + self._otherstrings = ['top', 'bottom'] + # this gets positioned w/o constrained_layout so exclude: + self._layoutbox = None + self._poslayoutbox = None + + self.set_location(location) + self.set_functions(functions) + + # styling: + if self._orientation == 'x': + otheraxis = self.yaxis + else: + otheraxis = self.xaxis + + 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) + + if self._pos < 0.5: + # flip the location strings... + self._locstrings = self._locstrings[::-1] + self.set_alignment(self._locstrings[0]) + + def set_alignment(self, align): + """ + Set if axes spine and labels are drawn at top or bottom (or left/right) + of the axes. + + Parameters + ---------- + align :: string + either 'top' or 'bottom' for orientation='x' or + 'left' or 'right' for orientation='y' axis + + """ + if align in self._locstrings: + if align == self._locstrings[1]: + # need to change the orientation. + self._locstrings = self._locstrings[::-1] + elif align != self._locstrings[0]: + raise ValueError('"{}" is not a valid axis orientation, ' + 'not changing the orientation;' + 'choose "{}" or "{}""'.format(align, + self._locstrings[0], self._locstrings[1])) + self.spines[self._locstrings[0]].set_visible(True) + self.spines[self._locstrings[1]].set_visible(False) + self._axis.set_ticks_position(align) + self._axis.set_label_position(align) + + def set_location(self, location): + """ + Set the vertical or horizontal location of the axes in + parent-normalized co-ordinates. + + Parameters + ---------- + location : string or scalar + The position to put the secondary axis. Strings can be 'top' or + 'bottom' for orientation='x' and 'right' or 'left' for + orientation='y', scalar can be a float indicating the relative + position on the parent axes to put the new axes, 0.0 being the + bottom (or left) and 1.0 being the top (or right). + """ + + # This puts the rectangle into figure-relative coordinates. + if isinstance(location, str): + if location in ['top', 'right']: + self._pos = 1. + elif location in ['bottom', 'left']: + self._pos = 0. + else: + raise ValueError("location must be '{}', '{}', or a " + "float, not '{}'".format(location, + self._locstrings[0], self._locstrings[1])) + else: + self._pos = location + self._loc = location + + if self._orientation == 'x': + bounds = [0, self._pos, 1., 1e-10] + else: + bounds = [self._pos, 0, 1e-10, 1] + + secondary_locator = _make_secondary_locator(bounds, self._parent) + + # this locator lets the axes move in the parent axes coordinates. + # so it never needs to know where the parent is explicitly in + # figure co-ordinates. + # it gets called in `ax.apply_aspect() (of all places) + self.set_axes_locator(secondary_locator) + + def apply_aspect(self, position=None): + self._set_lims() + super().apply_aspect(position) + + def set_ticks(self, ticks, minor=False): + """ + Set the x ticks with list of *ticks* + + Parameters + ---------- + ticks : list + List of x-axis tick locations. + + minor : bool, optional + If ``False`` sets major ticks, if ``True`` sets minor ticks. + Default is ``False``. + """ + ret = self._axis.set_ticks(ticks, minor=minor) + self.stale = True + self._ticks_set = True + return ret + + def set_functions(self, functions): + """ + Set how the secondary axis converts limits from the parent axes. + + Parameters + ---------- + functions : 2-tuple of func, or `Transform` with an inverse. + Transform between the parent axis values and the secondary axis + values. + + If supplied as a 2-tuple of functions, the first function is + the forward transform function and the second is the inverse + transform. + + If a transform is supplied, then the transform must have an + inverse. + + """ + + if self._orientation == 'x': + set_scale = self.set_xscale + parent_scale = self._parent.get_xscale() + else: + set_scale = self.set_yscale + parent_scale = self._parent.get_yscale() + # we need to use a modified scale so the scale can receive the + # transform. Only types supported are linear and log10 for now. + # Probably possible to add other transforms as a todo... + if parent_scale == 'log': + defscale = 'functionlog' + else: + defscale = 'function' + + if (isinstance(functions, tuple) and len(functions) == 2 and + callable(functions[0]) and callable(functions[1])): + # make an arbitrary convert from a two-tuple of functions + # forward and inverse. + self._functions = functions + elif functions is None: + self._functions = (lambda x: x, lambda x: x) + else: + raise ValueError('functions argument of secondary axes ' + 'must be a two-tuple of callable functions ' + 'with the first function being the transform ' + 'and the second being the inverse') + set_scale(defscale, functions=self._functions) + + def draw(self, renderer=None, inframe=False): + """ + Draw the secondary axes. + + Consults the parent axes for its limits and converts them + using the converter specified by + `~.axes._secondary_axes.set_functions` (or *functions* + parameter when axes initialized.) + + """ + + self._set_lims() + # this sets the scale in case the parent has set its scale. + self._set_scale() + super().draw(renderer=renderer, inframe=inframe) + + def _set_scale(self): + """ + Check if parent has set its scale + """ + + if self._orientation == 'x': + pscale = self._parent.xaxis.get_scale() + set_scale = self.set_xscale + if self._orientation == 'y': + pscale = self._parent.yaxis.get_scale() + set_scale = self.set_yscale + if pscale == 'log': + defscale = 'functionlog' + else: + defscale = 'function' + + if self._ticks_set: + ticks = self._axis.get_ticklocs() + + set_scale(defscale, functions=self._functions) + + # OK, set_scale sets the locators, but if we've called + # axsecond.set_ticks, we want to keep those. + if self._ticks_set: + self._axis.set_major_locator(FixedLocator(ticks)) + + def _set_lims(self): + """ + Set the limits based on parent limits and the convert method + between the parent and this secondary axes + """ + if self._orientation == 'x': + lims = self._parent.get_xlim() + set_lim = self.set_xlim + trans = self.xaxis.get_transform() + if self._orientation == 'y': + lims = self._parent.get_ylim() + set_lim = self.set_ylim + trans = self.yaxis.get_transform() + order = lims[0] < lims[1] + lims = self._functions[0](np.array(lims)) + neworder = lims[0] < lims[1] + if neworder != order: + # flip because the transform will take care of the flipping.. + lims = lims[::-1] + set_lim(lims) + + def get_tightbbox(self, renderer, call_axes_locator=True): + """ + Return the tight bounding box of the axes. + The dimension of the Bbox in canvas coordinate. + + If *call_axes_locator* is *False*, it does not call the + _axes_locator attribute, which is necessary to get the correct + bounding box. ``call_axes_locator==False`` can be used if the + caller is only intereted in the relative size of the tightbbox + compared to the axes bbox. + """ + + bb = [] + + if not self.get_visible(): + return None + + self._set_lims() + locator = self.get_axes_locator() + if locator and call_axes_locator: + pos = locator(self, renderer) + self.apply_aspect(pos) + else: + self.apply_aspect() + + if self._orientation == 'x': + bb_axis = self.xaxis.get_tightbbox(renderer) + else: + bb_axis = self.yaxis.get_tightbbox(renderer) + if bb_axis: + bb.append(bb_axis) + + bb.append(self.get_window_extent(renderer)) + _bbox = mtransforms.Bbox.union( + [b for b in bb if b.width != 0 or b.height != 0]) + + return _bbox + + def set_aspect(self, *args, **kwargs): + """ + Secondary axes cannot set the aspect ratio, so calling this just + sets a warning. + """ + cbook._warn_external("Secondary axes can't set the aspect ratio") + + def set_xlabel(self, xlabel, fontdict=None, labelpad=None, **kwargs): + """ + Set the label for the x-axis. + + Parameters + ---------- + xlabel : str + The label text. + + labelpad : scalar, optional, default: None + Spacing in points between the label and the x-axis. + + Other Parameters + ---------------- + **kwargs : `.Text` properties + `.Text` properties control the appearance of the label. + + See also + -------- + text : for information on how override and the optional args work + """ + if labelpad is not None: + self.xaxis.labelpad = labelpad + return self.xaxis.set_label_text(xlabel, fontdict, **kwargs) + + def set_ylabel(self, ylabel, fontdict=None, labelpad=None, **kwargs): + """ + Set the label for the x-axis. + + Parameters + ---------- + ylabel : str + The label text. + + labelpad : scalar, optional, default: None + Spacing in points between the label and the x-axis. + + Other Parameters + ---------------- + **kwargs : `.Text` properties + `.Text` properties control the appearance of the label. + + See also + -------- + text : for information on how override and the optional args work + """ + if labelpad is not None: + self.yaxis.labelpad = labelpad + return self.yaxis.set_label_text(ylabel, fontdict, **kwargs) + + def set_color(self, color): + """ + Change the color of the secondary axes and all decorators + Parameters + ---------- + color : Matplotlib 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.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.yaxis.label.set_color(color) + + +_secax_docstring = ''' +Warnings +-------- + +This method is experimental as of 3.1, and the API may change. + +Parameters +---------- +location : string or scalar + The position to put the secondary axis. Strings can be 'top' or + 'bottom', for x-oriented axises or 'left' or 'right' for y-oriented axises + or a scalar can be a float indicating the relative position + on the axes to put the new axes (0 being the bottom (left), and 1.0 being + the top (right).) + +functions : 2-tuple of func, or Transform with an inverse + + If a 2-tuple of functions, the user specifies the transform + function and its inverse. i.e. + `functions=(lamda x: 2 / x, lambda x: 2 / x)` would be an + reciprocal transform with a factor of 2. + + The user can also directly supply a subclass of + `.transforms.Transform` so long as it has an inverse. + + See :doc:`/gallery/subplots_axes_and_figures/secondary_axis` + for examples of making these conversions. + + +Other Parameters +---------------- +**kwargs : `~matplotlib.axes.Axes` properties. + Other miscellaneous axes parameters. + +Returns +------- +ax : axes._secondary_axes.SecondaryAxis +''' +docstring.interpd.update(_secax_docstring=_secax_docstring) diff --git a/lib/matplotlib/axis.py b/lib/matplotlib/axis.py index 127c9c5b0fb3..e1e1ff61cd02 100644 --- a/lib/matplotlib/axis.py +++ b/lib/matplotlib/axis.py @@ -1159,11 +1159,11 @@ def get_tightbbox(self, renderer): if (np.isfinite(bbox.width) and np.isfinite(bbox.height) and a.get_visible()): bb.append(bbox) - bb.extend(ticklabelBoxes) bb.extend(ticklabelBoxes2) - - bb = [b for b in bb if b.width != 0 or b.height != 0] + bb = [b for b in bb if ((b.width != 0 or b.height != 0) and + np.isfinite(b.width) and + np.isfinite(b.height))] if bb: _bbox = mtransforms.Bbox.union(bb) return _bbox diff --git a/lib/matplotlib/dates.py b/lib/matplotlib/dates.py index dbfcae354baa..915791db71c0 100644 --- a/lib/matplotlib/dates.py +++ b/lib/matplotlib/dates.py @@ -309,7 +309,6 @@ def _from_ordinalf(x, tz=None): # add hours, minutes, seconds, microseconds dt += datetime.timedelta(microseconds=remainder_musec) - return dt.astimezone(tz) diff --git a/lib/matplotlib/scale.py b/lib/matplotlib/scale.py index ddd1bcbcd3d2..fb55d33156a0 100644 --- a/lib/matplotlib/scale.py +++ b/lib/matplotlib/scale.py @@ -409,6 +409,47 @@ def limit_range_for_scale(self, vmin, vmax, minpos): minpos if vmax <= 0 else vmax) +class FuncScaleLog(LogScale): + """ + Provide an arbitrary scale with user-supplied function for the axis and + then put on a logarithmic axes + """ + + name = 'functionlog' + + def __init__(self, axis, functions, base=10): + """ + Parameters + ---------- + + axis: the axis for the scale + + functions : (callable, callable) + two-tuple of the forward and inverse functions for the scale. + The forward function must have an inverse and, for best behavior, + be monotonic. + + Both functions must have the signature:: + + def forward(values: array-like) -> array-like + + base : float + logarithmic base of the scale (default = 10) + + """ + forward, inverse = functions + self.base = base + self.subs = None + transform = FuncTransform(forward, inverse) + LogTransform(base) + self._transform = transform + + def get_transform(self): + """ + The transform for arbitrary scaling + """ + return self._transform + + class SymmetricalLogTransform(Transform): input_dims = 1 output_dims = 1 @@ -660,6 +701,7 @@ def limit_range_for_scale(self, vmin, vmax, minpos): 'symlog': SymmetricalLogScale, 'logit': LogitScale, 'function': FuncScale, + 'functionlog': FuncScaleLog, } diff --git a/lib/matplotlib/tests/baseline_images/test_axes/secondary_xy.png b/lib/matplotlib/tests/baseline_images/test_axes/secondary_xy.png new file mode 100644 index 0000000000000000000000000000000000000000..2e91f63326eedb185fb2f5637c3801a4b0739dfb GIT binary patch literal 42332 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 Date: Tue, 5 Feb 2019 07:39:40 -0800 Subject: [PATCH 2/3] FIX: more fixes --- .../secondary_axis.py | 23 ++++--------------- lib/matplotlib/scale.py | 6 ++--- lib/mpl_toolkits/mplot3d/axes3d.py | 2 +- 3 files changed, 7 insertions(+), 24 deletions(-) diff --git a/examples/subplots_axes_and_figures/secondary_axis.py b/examples/subplots_axes_and_figures/secondary_axis.py index 466b1a2f8e32..caa27a9c895f 100644 --- a/examples/subplots_axes_and_figures/secondary_axis.py +++ b/examples/subplots_axes_and_figures/secondary_axis.py @@ -6,9 +6,9 @@ Sometimes we want as secondary axis on a plot, for instance to convert radians to degrees on the same plot. We can do this by making a child axes with only one axis visible via `.Axes.axes.secondary_xaxis` and -`.Axes.axes.secondary_yaxis`. - -If we want to label the top of the axes: +`.Axes.axes.secondary_yaxis`. This secondary axis can have a different scale +than the main axis by providing both a forward and an inverse conversion +function in a tuple to the ``functions`` kwarg: """ import matplotlib.pyplot as plt @@ -26,22 +26,6 @@ ax.set_xlabel('angle [degrees]') ax.set_ylabel('signal') ax.set_title('Sine wave') -secax = ax.secondary_xaxis('top') -plt.show() - -########################################################################### -# However, its often useful to label the secondary axis with something -# other than the labels in the main axis. In that case we need to provide -# both a forward and an inverse conversion function in a tuple -# to the ``functions`` kwarg: - -fig, ax = plt.subplots(constrained_layout=True) -x = np.arange(0, 360, 1) -y = np.sin(2 * x * np.pi / 180) -ax.plot(x, y) -ax.set_xlabel('angle [degrees]') -ax.set_ylabel('signal') -ax.set_title('Sine wave') def deg2rad(x): @@ -98,6 +82,7 @@ def inverse(x): xold = np.arange(0, 11, 0.2) # fake data set relating x co-ordinate to another data-derived co-ordinate. +# xnew must be monotonic, so we sort... xnew = np.sort(10 * np.exp(-xold / 4) + np.random.randn(len(xold)) / 3) ax.plot(xold[3:], xnew[3:], label='Transform data') diff --git a/lib/matplotlib/scale.py b/lib/matplotlib/scale.py index fb55d33156a0..d70049ec3b8b 100644 --- a/lib/matplotlib/scale.py +++ b/lib/matplotlib/scale.py @@ -163,8 +163,7 @@ def __init__(self, axis, functions): functions : (callable, callable) two-tuple of the forward and inverse functions for the scale. - The forward function must have an inverse and, for best behavior, - be monotonic. + The forward function must be monotonic Both functions must have the signature:: @@ -426,8 +425,7 @@ def __init__(self, axis, functions, base=10): functions : (callable, callable) two-tuple of the forward and inverse functions for the scale. - The forward function must have an inverse and, for best behavior, - be monotonic. + The forward function must be monotonic. Both functions must have the signature:: diff --git a/lib/mpl_toolkits/mplot3d/axes3d.py b/lib/mpl_toolkits/mplot3d/axes3d.py index fc4f83ecf697..fe644d9cb967 100644 --- a/lib/mpl_toolkits/mplot3d/axes3d.py +++ b/lib/mpl_toolkits/mplot3d/axes3d.py @@ -829,7 +829,7 @@ def set_yscale(self, value, **kwargs): def set_zscale(self, value, **kwargs): """ - Set the x-axis scale. + Set the z-axis scale. Parameters ---------- From 85c1b99efb7eb22221067aebd6fe13e7e8b88837 Mon Sep 17 00:00:00 2001 From: Jody Klymak Date: Fri, 8 Feb 2019 07:07:15 -0800 Subject: [PATCH 3/3] FIX --- lib/matplotlib/axes/_axes.py | 3 +-- lib/matplotlib/axes/_secondary_axes.py | 2 +- lib/matplotlib/scale.py | 4 ++-- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index 13e8cda4e622..a6238e0c6ae0 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -613,8 +613,7 @@ def secondary_xaxis(self, location, *, functions=None, **kwargs): Examples -------- - Add a secondary axes that shows both wavelength for the main - axes that shows wavenumber. + The main axis shows frequency, and the secondary axis shows period. .. plot:: diff --git a/lib/matplotlib/axes/_secondary_axes.py b/lib/matplotlib/axes/_secondary_axes.py index 83960601c260..3cbe8e0f1ff1 100644 --- a/lib/matplotlib/axes/_secondary_axes.py +++ b/lib/matplotlib/axes/_secondary_axes.py @@ -441,7 +441,7 @@ def set_color(self, color): If a 2-tuple of functions, the user specifies the transform function and its inverse. i.e. - `functions=(lamda x: 2 / x, lambda x: 2 / x)` would be an + `functions=(lambda x: 2 / x, lambda x: 2 / x)` would be an reciprocal transform with a factor of 2. The user can also directly supply a subclass of diff --git a/lib/matplotlib/scale.py b/lib/matplotlib/scale.py index d70049ec3b8b..8ce9b68a7165 100644 --- a/lib/matplotlib/scale.py +++ b/lib/matplotlib/scale.py @@ -163,7 +163,7 @@ def __init__(self, axis, functions): functions : (callable, callable) two-tuple of the forward and inverse functions for the scale. - The forward function must be monotonic + The forward function must be monotonic. Both functions must have the signature:: @@ -411,7 +411,7 @@ def limit_range_for_scale(self, vmin, vmax, minpos): class FuncScaleLog(LogScale): """ Provide an arbitrary scale with user-supplied function for the axis and - then put on a logarithmic axes + then put on a logarithmic axes. """ name = 'functionlog'