From 87c742b99dc6b9a190f8c89bc6256ced72f5ab80 Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Tue, 9 Jul 2019 00:43:37 +0200 Subject: [PATCH] Fix axes aspect for non-linear, non-log, possibly mixed-scale axes. The main change is to make Axes.get_data_ratio take axes scales into account. This is a breaking change in get_data_ratio, but also the most reasonable way I could think of to implement the feature while also supporting third-party Axes subclasses that override this method (given that it is explicitly documented as being overridable for this purpose). (Compare, for example, with a patch that also deprecates get_data_ratio and moves the whole computation to apply_aspect; now what do we do with third-party overrides?) Also move the adjustable="datalim"-part of the implementation of apply_aspect down one indentation block for symmetry with adjustable="box". The change in test_log_scale_image is because we can't rely on aspect=1 not being implemented for semilog plots anymore... --- doc/api/next_api_changes/2019-07-09-AL.rst | 8 ++ lib/matplotlib/axes/_base.py | 104 +++++++-------------- lib/matplotlib/tests/test_axes.py | 26 ++++++ lib/matplotlib/tests/test_image.py | 9 +- 4 files changed, 73 insertions(+), 74 deletions(-) create mode 100644 doc/api/next_api_changes/2019-07-09-AL.rst diff --git a/doc/api/next_api_changes/2019-07-09-AL.rst b/doc/api/next_api_changes/2019-07-09-AL.rst new file mode 100644 index 000000000000..b9b97a6ca9db --- /dev/null +++ b/doc/api/next_api_changes/2019-07-09-AL.rst @@ -0,0 +1,8 @@ +API changes +``````````` + +``Axes.get_data_ratio`` now takes the axes scale into account (linear, log, +logit, etc.) before computing the y-to-x ratio. This change allows fixed +aspects to be applied to any combination of x and y scales. + +``Axes.get_data_ratio_log`` is deprecated. diff --git a/lib/matplotlib/axes/_base.py b/lib/matplotlib/axes/_base.py index 16d8d7ae9a5d..294e05548592 100644 --- a/lib/matplotlib/axes/_base.py +++ b/lib/matplotlib/axes/_base.py @@ -1396,20 +1396,21 @@ def set_anchor(self, anchor, share=False): def get_data_ratio(self): """ - Return the aspect ratio of the raw data. + Return the aspect ratio of the scaled data. Notes ----- This method is intended to be overridden by new projection types. """ - xmin, xmax = self.get_xbound() - ymin, ymax = self.get_ybound() - - xsize = max(abs(xmax - xmin), 1e-30) - ysize = max(abs(ymax - ymin), 1e-30) - + trf_xmin, trf_xmax = map( + self.xaxis.get_transform().transform, self.get_xbound()) + trf_ymin, trf_ymax = map( + self.yaxis.get_transform().transform, self.get_ybound()) + xsize = max(abs(trf_xmax - trf_xmin), 1e-30) + ysize = max(abs(trf_ymax - trf_ymin), 1e-30) return ysize / xsize + @cbook.deprecated("3.2") def get_data_ratio_log(self): """ Return the aspect ratio of the raw data in log scale. @@ -1455,81 +1456,53 @@ def apply_aspect(self, position=None): aspect = self.get_aspect() - if self.name != 'polar': - xscale, yscale = self.get_xscale(), self.get_yscale() - if xscale == "linear" and yscale == "linear": - aspect_scale_mode = "linear" - elif xscale == "log" and yscale == "log": - aspect_scale_mode = "log" - elif ((xscale == "linear" and yscale == "log") or - (xscale == "log" and yscale == "linear")): - if aspect != "auto": - cbook._warn_external( - 'aspect is not supported for Axes with xscale=%s, ' - 'yscale=%s' % (xscale, yscale)) - aspect = "auto" - else: # some custom projections have their own scales. - pass - else: - aspect_scale_mode = "linear" - if aspect == 'auto': self._set_position(position, which='active') return if aspect == 'equal': - A = 1 - else: - A = aspect + aspect = 1 + + fig_width, fig_height = self.get_figure().get_size_inches() + fig_aspect = fig_height / fig_width - figW, figH = self.get_figure().get_size_inches() - fig_aspect = figH / figW if self._adjustable == 'box': if self in self._twinned_axes: - raise RuntimeError("Adjustable 'box' is not allowed in a" - " twinned Axes. Use 'datalim' instead.") - if aspect_scale_mode == "log": - box_aspect = A * self.get_data_ratio_log() - else: - box_aspect = A * self.get_data_ratio() + raise RuntimeError("Adjustable 'box' is not allowed in a " + "twinned Axes; use 'datalim' instead") + box_aspect = aspect * self.get_data_ratio() pb = position.frozen() pb1 = pb.shrunk_to_aspect(box_aspect, pb, fig_aspect) self._set_position(pb1.anchored(self.get_anchor(), pb), 'active') return - # reset active to original in case it had been changed - # by prior use of 'box' - self._set_position(position, which='active') - - xmin, xmax = self.get_xbound() - ymin, ymax = self.get_ybound() + # self._adjustable == 'datalim' - if aspect_scale_mode == "log": - xmin, xmax = math.log10(xmin), math.log10(xmax) - ymin, ymax = math.log10(ymin), math.log10(ymax) + # reset active to original in case it had been changed by prior use + # of 'box' + self._set_position(position, which='active') + x_trf = self.xaxis.get_transform() + y_trf = self.yaxis.get_transform() + xmin, xmax = map(x_trf.transform, self.get_xbound()) + ymin, ymax = map(y_trf.transform, self.get_ybound()) xsize = max(abs(xmax - xmin), 1e-30) ysize = max(abs(ymax - ymin), 1e-30) l, b, w, h = position.bounds box_aspect = fig_aspect * (h / w) - data_ratio = box_aspect / A + data_ratio = box_aspect / aspect - y_expander = (data_ratio * xsize / ysize - 1.0) + y_expander = data_ratio * xsize / ysize - 1 # If y_expander > 0, the dy/dx viewLim ratio needs to increase if abs(y_expander) < 0.005: return - if aspect_scale_mode == "log": - dL = self.dataLim - dL_width = math.log10(dL.x1) - math.log10(dL.x0) - dL_height = math.log10(dL.y1) - math.log10(dL.y0) - xr = 1.05 * dL_width - yr = 1.05 * dL_height - else: - dL = self.dataLim - xr = 1.05 * dL.width - yr = 1.05 * dL.height + dL = self.dataLim + x0, x1 = map(x_trf.inverted().transform, dL.intervalx) + y0, y1 = map(y_trf.inverted().transform, dL.intervaly) + xr = 1.05 * (x1 - x0) + yr = 1.05 * (y1 - y0) xmarg = xsize - xr ymarg = ysize - yr @@ -1537,8 +1510,7 @@ def apply_aspect(self, position=None): Xsize = ysize / data_ratio Xmarg = Xsize - xr Ymarg = Ysize - yr - # Setting these targets to, e.g., 0.05*xr does not seem to - # help. + # Setting these targets to, e.g., 0.05*xr does not seem to help. xm = 0 ym = 0 @@ -1546,8 +1518,8 @@ def apply_aspect(self, position=None): shared_y = self in self._shared_y_axes # Not sure whether we need this check: if shared_x and shared_y: - raise RuntimeError("adjustable='datalim' is not allowed when both" - " axes are shared.") + raise RuntimeError("adjustable='datalim' is not allowed when both " + "axes are shared") # If y is shared, then we are only allowed to change x, etc. if shared_y: @@ -1564,18 +1536,12 @@ def apply_aspect(self, position=None): yc = 0.5 * (ymin + ymax) y0 = yc - Ysize / 2.0 y1 = yc + Ysize / 2.0 - if aspect_scale_mode == "log": - self.set_ybound((10. ** y0, 10. ** y1)) - else: - self.set_ybound((y0, y1)) + self.set_ybound(*map(y_trf.inverted().transform, (y0, y1))) else: xc = 0.5 * (xmin + xmax) x0 = xc - Xsize / 2.0 x1 = xc + Xsize / 2.0 - if aspect_scale_mode == "log": - self.set_xbound((10. ** x0, 10. ** x1)) - else: - self.set_xbound((x0, x1)) + self.set_xbound(*map(x_trf.inverted().transform, (x0, x1))) def axis(self, *args, emit=True, **kwargs): """ diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py index f1adb4e4d692..4fd7a4d12a08 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -6495,3 +6495,29 @@ def test_set_ticks_inverted(): ax.invert_xaxis() ax.set_xticks([.3, .7]) assert ax.get_xlim() == (1, 0) + + +def test_aspect_nonlinear_adjustable_box(): + fig = plt.figure(figsize=(10, 10)) # Square. + + ax = fig.add_subplot() + ax.plot([.4, .6], [.4, .6]) # Set minpos to keep logit happy. + ax.set(xscale="log", xlim=(1, 10), + yscale="logit", ylim=(1/11, 1/1001), + aspect=1, adjustable="box") + ax.margins(0) + pos = fig.transFigure.transform_bbox(ax.get_position()) + assert pos.height / pos.width == pytest.approx(2) + + +def test_aspect_nonlinear_adjustable_datalim(): + fig = plt.figure(figsize=(10, 10)) # Square. + + ax = fig.add_axes([.1, .1, .8, .8]) # Square. + ax.plot([.4, .6], [.4, .6]) # Set minpos to keep logit happy. + ax.set(xscale="log", xlim=(1, 10), + yscale="logit", ylim=(1/11, 1/1001), + aspect=1, adjustable="datalim") + ax.margins(0) + ax.apply_aspect() + assert ax.get_xlim() == pytest.approx(np.array([1/10, 10]) * np.sqrt(10)) diff --git a/lib/matplotlib/tests/test_image.py b/lib/matplotlib/tests/test_image.py index f001b0d2db3f..18d513b2c962 100644 --- a/lib/matplotlib/tests/test_image.py +++ b/lib/matplotlib/tests/test_image.py @@ -686,15 +686,14 @@ def test_load_from_url(): @image_comparison(['log_scale_image'], remove_text=True) -# The recwarn fixture captures a warning in image_comparison. -def test_log_scale_image(recwarn): +def test_log_scale_image(): Z = np.zeros((10, 10)) Z[::2] = 1 fig, ax = plt.subplots() - ax.imshow(Z, extent=[1, 100, 1, 100], cmap='viridis', - vmax=1, vmin=-1) - ax.set_yscale('log') + ax.imshow(Z, extent=[1, 100, 1, 100], cmap='viridis', vmax=1, vmin=-1, + aspect='auto') + ax.set(yscale='log') @image_comparison(['rotate_image'], remove_text=True) pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy