Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 87c742b

Browse files
committed
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...
1 parent 9f1c730 commit 87c742b

File tree

4 files changed

+73
-74
lines changed

4 files changed

+73
-74
lines changed
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
API changes
2+
```````````
3+
4+
``Axes.get_data_ratio`` now takes the axes scale into account (linear, log,
5+
logit, etc.) before computing the y-to-x ratio. This change allows fixed
6+
aspects to be applied to any combination of x and y scales.
7+
8+
``Axes.get_data_ratio_log`` is deprecated.

lib/matplotlib/axes/_base.py

Lines changed: 35 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -1396,20 +1396,21 @@ def set_anchor(self, anchor, share=False):
13961396

13971397
def get_data_ratio(self):
13981398
"""
1399-
Return the aspect ratio of the raw data.
1399+
Return the aspect ratio of the scaled data.
14001400
14011401
Notes
14021402
-----
14031403
This method is intended to be overridden by new projection types.
14041404
"""
1405-
xmin, xmax = self.get_xbound()
1406-
ymin, ymax = self.get_ybound()
1407-
1408-
xsize = max(abs(xmax - xmin), 1e-30)
1409-
ysize = max(abs(ymax - ymin), 1e-30)
1410-
1405+
trf_xmin, trf_xmax = map(
1406+
self.xaxis.get_transform().transform, self.get_xbound())
1407+
trf_ymin, trf_ymax = map(
1408+
self.yaxis.get_transform().transform, self.get_ybound())
1409+
xsize = max(abs(trf_xmax - trf_xmin), 1e-30)
1410+
ysize = max(abs(trf_ymax - trf_ymin), 1e-30)
14111411
return ysize / xsize
14121412

1413+
@cbook.deprecated("3.2")
14131414
def get_data_ratio_log(self):
14141415
"""
14151416
Return the aspect ratio of the raw data in log scale.
@@ -1455,99 +1456,70 @@ def apply_aspect(self, position=None):
14551456

14561457
aspect = self.get_aspect()
14571458

1458-
if self.name != 'polar':
1459-
xscale, yscale = self.get_xscale(), self.get_yscale()
1460-
if xscale == "linear" and yscale == "linear":
1461-
aspect_scale_mode = "linear"
1462-
elif xscale == "log" and yscale == "log":
1463-
aspect_scale_mode = "log"
1464-
elif ((xscale == "linear" and yscale == "log") or
1465-
(xscale == "log" and yscale == "linear")):
1466-
if aspect != "auto":
1467-
cbook._warn_external(
1468-
'aspect is not supported for Axes with xscale=%s, '
1469-
'yscale=%s' % (xscale, yscale))
1470-
aspect = "auto"
1471-
else: # some custom projections have their own scales.
1472-
pass
1473-
else:
1474-
aspect_scale_mode = "linear"
1475-
14761459
if aspect == 'auto':
14771460
self._set_position(position, which='active')
14781461
return
14791462

14801463
if aspect == 'equal':
1481-
A = 1
1482-
else:
1483-
A = aspect
1464+
aspect = 1
1465+
1466+
fig_width, fig_height = self.get_figure().get_size_inches()
1467+
fig_aspect = fig_height / fig_width
14841468

1485-
figW, figH = self.get_figure().get_size_inches()
1486-
fig_aspect = figH / figW
14871469
if self._adjustable == 'box':
14881470
if self in self._twinned_axes:
1489-
raise RuntimeError("Adjustable 'box' is not allowed in a"
1490-
" twinned Axes. Use 'datalim' instead.")
1491-
if aspect_scale_mode == "log":
1492-
box_aspect = A * self.get_data_ratio_log()
1493-
else:
1494-
box_aspect = A * self.get_data_ratio()
1471+
raise RuntimeError("Adjustable 'box' is not allowed in a "
1472+
"twinned Axes; use 'datalim' instead")
1473+
box_aspect = aspect * self.get_data_ratio()
14951474
pb = position.frozen()
14961475
pb1 = pb.shrunk_to_aspect(box_aspect, pb, fig_aspect)
14971476
self._set_position(pb1.anchored(self.get_anchor(), pb), 'active')
14981477
return
14991478

1500-
# reset active to original in case it had been changed
1501-
# by prior use of 'box'
1502-
self._set_position(position, which='active')
1503-
1504-
xmin, xmax = self.get_xbound()
1505-
ymin, ymax = self.get_ybound()
1479+
# self._adjustable == 'datalim'
15061480

1507-
if aspect_scale_mode == "log":
1508-
xmin, xmax = math.log10(xmin), math.log10(xmax)
1509-
ymin, ymax = math.log10(ymin), math.log10(ymax)
1481+
# reset active to original in case it had been changed by prior use
1482+
# of 'box'
1483+
self._set_position(position, which='active')
15101484

1485+
x_trf = self.xaxis.get_transform()
1486+
y_trf = self.yaxis.get_transform()
1487+
xmin, xmax = map(x_trf.transform, self.get_xbound())
1488+
ymin, ymax = map(y_trf.transform, self.get_ybound())
15111489
xsize = max(abs(xmax - xmin), 1e-30)
15121490
ysize = max(abs(ymax - ymin), 1e-30)
15131491

15141492
l, b, w, h = position.bounds
15151493
box_aspect = fig_aspect * (h / w)
1516-
data_ratio = box_aspect / A
1494+
data_ratio = box_aspect / aspect
15171495

1518-
y_expander = (data_ratio * xsize / ysize - 1.0)
1496+
y_expander = data_ratio * xsize / ysize - 1
15191497
# If y_expander > 0, the dy/dx viewLim ratio needs to increase
15201498
if abs(y_expander) < 0.005:
15211499
return
15221500

1523-
if aspect_scale_mode == "log":
1524-
dL = self.dataLim
1525-
dL_width = math.log10(dL.x1) - math.log10(dL.x0)
1526-
dL_height = math.log10(dL.y1) - math.log10(dL.y0)
1527-
xr = 1.05 * dL_width
1528-
yr = 1.05 * dL_height
1529-
else:
1530-
dL = self.dataLim
1531-
xr = 1.05 * dL.width
1532-
yr = 1.05 * dL.height
1501+
dL = self.dataLim
1502+
x0, x1 = map(x_trf.inverted().transform, dL.intervalx)
1503+
y0, y1 = map(y_trf.inverted().transform, dL.intervaly)
1504+
xr = 1.05 * (x1 - x0)
1505+
yr = 1.05 * (y1 - y0)
15331506

15341507
xmarg = xsize - xr
15351508
ymarg = ysize - yr
15361509
Ysize = data_ratio * xsize
15371510
Xsize = ysize / data_ratio
15381511
Xmarg = Xsize - xr
15391512
Ymarg = Ysize - yr
1540-
# Setting these targets to, e.g., 0.05*xr does not seem to
1541-
# help.
1513+
# Setting these targets to, e.g., 0.05*xr does not seem to help.
15421514
xm = 0
15431515
ym = 0
15441516

15451517
shared_x = self in self._shared_x_axes
15461518
shared_y = self in self._shared_y_axes
15471519
# Not sure whether we need this check:
15481520
if shared_x and shared_y:
1549-
raise RuntimeError("adjustable='datalim' is not allowed when both"
1550-
" axes are shared.")
1521+
raise RuntimeError("adjustable='datalim' is not allowed when both "
1522+
"axes are shared")
15511523

15521524
# If y is shared, then we are only allowed to change x, etc.
15531525
if shared_y:
@@ -1564,18 +1536,12 @@ def apply_aspect(self, position=None):
15641536
yc = 0.5 * (ymin + ymax)
15651537
y0 = yc - Ysize / 2.0
15661538
y1 = yc + Ysize / 2.0
1567-
if aspect_scale_mode == "log":
1568-
self.set_ybound((10. ** y0, 10. ** y1))
1569-
else:
1570-
self.set_ybound((y0, y1))
1539+
self.set_ybound(*map(y_trf.inverted().transform, (y0, y1)))
15711540
else:
15721541
xc = 0.5 * (xmin + xmax)
15731542
x0 = xc - Xsize / 2.0
15741543
x1 = xc + Xsize / 2.0
1575-
if aspect_scale_mode == "log":
1576-
self.set_xbound((10. ** x0, 10. ** x1))
1577-
else:
1578-
self.set_xbound((x0, x1))
1544+
self.set_xbound(*map(x_trf.inverted().transform, (x0, x1)))
15791545

15801546
def axis(self, *args, emit=True, **kwargs):
15811547
"""

lib/matplotlib/tests/test_axes.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6495,3 +6495,29 @@ def test_set_ticks_inverted():
64956495
ax.invert_xaxis()
64966496
ax.set_xticks([.3, .7])
64976497
assert ax.get_xlim() == (1, 0)
6498+
6499+
6500+
def test_aspect_nonlinear_adjustable_box():
6501+
fig = plt.figure(figsize=(10, 10)) # Square.
6502+
6503+
ax = fig.add_subplot()
6504+
ax.plot([.4, .6], [.4, .6]) # Set minpos to keep logit happy.
6505+
ax.set(xscale="log", xlim=(1, 10),
6506+
yscale="logit", ylim=(1/11, 1/1001),
6507+
aspect=1, adjustable="box")
6508+
ax.margins(0)
6509+
pos = fig.transFigure.transform_bbox(ax.get_position())
6510+
assert pos.height / pos.width == pytest.approx(2)
6511+
6512+
6513+
def test_aspect_nonlinear_adjustable_datalim():
6514+
fig = plt.figure(figsize=(10, 10)) # Square.
6515+
6516+
ax = fig.add_axes([.1, .1, .8, .8]) # Square.
6517+
ax.plot([.4, .6], [.4, .6]) # Set minpos to keep logit happy.
6518+
ax.set(xscale="log", xlim=(1, 10),
6519+
yscale="logit", ylim=(1/11, 1/1001),
6520+
aspect=1, adjustable="datalim")
6521+
ax.margins(0)
6522+
ax.apply_aspect()
6523+
assert ax.get_xlim() == pytest.approx(np.array([1/10, 10]) * np.sqrt(10))

lib/matplotlib/tests/test_image.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -686,15 +686,14 @@ def test_load_from_url():
686686

687687

688688
@image_comparison(['log_scale_image'], remove_text=True)
689-
# The recwarn fixture captures a warning in image_comparison.
690-
def test_log_scale_image(recwarn):
689+
def test_log_scale_image():
691690
Z = np.zeros((10, 10))
692691
Z[::2] = 1
693692

694693
fig, ax = plt.subplots()
695-
ax.imshow(Z, extent=[1, 100, 1, 100], cmap='viridis',
696-
vmax=1, vmin=-1)
697-
ax.set_yscale('log')
694+
ax.imshow(Z, extent=[1, 100, 1, 100], cmap='viridis', vmax=1, vmin=-1,
695+
aspect='auto')
696+
ax.set(yscale='log')
698697

699698

700699
@image_comparison(['rotate_image'], remove_text=True)

0 commit comments

Comments
 (0)