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)