Skip to content

Commit 5655a16

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".
1 parent c19ebd6 commit 5655a16

File tree

3 files changed

+107
-109
lines changed

3 files changed

+107
-109
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: 73 additions & 109 deletions
Original file line numberDiff line numberDiff line change
@@ -1433,20 +1433,21 @@ def set_anchor(self, anchor, share=False):
14331433

14341434
def get_data_ratio(self):
14351435
"""
1436-
Return the aspect ratio of the raw data.
1436+
Return the aspect ratio of the scaled data.
14371437
14381438
Notes
14391439
-----
14401440
This method is intended to be overridden by new projection types.
14411441
"""
1442-
xmin, xmax = self.get_xbound()
1443-
ymin, ymax = self.get_ybound()
1444-
1445-
xsize = max(abs(xmax - xmin), 1e-30)
1446-
ysize = max(abs(ymax - ymin), 1e-30)
1447-
1442+
trf_xmin, trf_xmax = map(
1443+
self.xaxis.get_transform().transform, self.get_xbound())
1444+
trf_ymin, trf_ymax = map(
1445+
self.yaxis.get_transform().transform, self.get_ybound())
1446+
xsize = max(abs(trf_xmax - trf_xmin), 1e-30)
1447+
ysize = max(abs(trf_ymax - trf_ymin), 1e-30)
14481448
return ysize / xsize
14491449

1450+
@cbook.deprecated("3.2")
14501451
def get_data_ratio_log(self):
14511452
"""
14521453
Return the aspect ratio of the raw data in log scale.
@@ -1492,127 +1493,90 @@ def apply_aspect(self, position=None):
14921493

14931494
aspect = self.get_aspect()
14941495

1495-
if self.name != 'polar':
1496-
xscale, yscale = self.get_xscale(), self.get_yscale()
1497-
if xscale == "linear" and yscale == "linear":
1498-
aspect_scale_mode = "linear"
1499-
elif xscale == "log" and yscale == "log":
1500-
aspect_scale_mode = "log"
1501-
elif ((xscale == "linear" and yscale == "log") or
1502-
(xscale == "log" and yscale == "linear")):
1503-
if aspect != "auto":
1504-
cbook._warn_external(
1505-
'aspect is not supported for Axes with xscale=%s, '
1506-
'yscale=%s' % (xscale, yscale))
1507-
aspect = "auto"
1508-
else: # some custom projections have their own scales.
1509-
pass
1510-
else:
1511-
aspect_scale_mode = "linear"
1512-
15131496
if aspect == 'auto':
15141497
self._set_position(position, which='active')
15151498
return
15161499

15171500
if aspect == 'equal':
1518-
A = 1
1519-
else:
1520-
A = aspect
1501+
aspect = 1
1502+
1503+
fig_width, fig_height = self.get_figure().get_size_inches()
1504+
fig_aspect = fig_height / fig_width
15211505

1522-
figW, figH = self.get_figure().get_size_inches()
1523-
fig_aspect = figH / figW
15241506
if self._adjustable == 'box':
15251507
if self in self._twinned_axes:
1526-
raise RuntimeError("Adjustable 'box' is not allowed in a"
1527-
" twinned Axes. Use 'datalim' instead.")
1528-
if aspect_scale_mode == "log":
1529-
box_aspect = A * self.get_data_ratio_log()
1530-
else:
1531-
box_aspect = A * self.get_data_ratio()
1508+
raise RuntimeError("Adjustable 'box' is not allowed in a "
1509+
"twinned Axes; use 'datalim' instead")
1510+
box_aspect = aspect * self.get_data_ratio()
15321511
pb = position.frozen()
15331512
pb1 = pb.shrunk_to_aspect(box_aspect, pb, fig_aspect)
15341513
self._set_position(pb1.anchored(self.get_anchor(), pb), 'active')
1535-
return
1536-
1537-
# reset active to original in case it had been changed
1538-
# by prior use of 'box'
1539-
self._set_position(position, which='active')
15401514

1541-
xmin, xmax = self.get_xbound()
1542-
ymin, ymax = self.get_ybound()
1543-
1544-
if aspect_scale_mode == "log":
1545-
xmin, xmax = math.log10(xmin), math.log10(xmax)
1546-
ymin, ymax = math.log10(ymin), math.log10(ymax)
1515+
elif self._adjustable == 'datalim':
1516+
# reset active to original in case it had been changed by prior use
1517+
# of 'box'
1518+
self._set_position(position, which='active')
15471519

1548-
xsize = max(abs(xmax - xmin), 1e-30)
1549-
ysize = max(abs(ymax - ymin), 1e-30)
1520+
x_trf = self.xaxis.get_transform()
1521+
y_trf = self.yaxis.get_transform()
1522+
xmin, xmax = map(x_trf.transform, self.get_xbound())
1523+
ymin, ymax = map(y_trf.transform, self.get_ybound())
1524+
xsize = max(abs(xmax - xmin), 1e-30)
1525+
ysize = max(abs(ymax - ymin), 1e-30)
15501526

1551-
l, b, w, h = position.bounds
1552-
box_aspect = fig_aspect * (h / w)
1553-
data_ratio = box_aspect / A
1527+
l, b, w, h = position.bounds
1528+
box_aspect = fig_aspect * (h / w)
1529+
data_ratio = box_aspect / aspect
15541530

1555-
y_expander = (data_ratio * xsize / ysize - 1.0)
1556-
# If y_expander > 0, the dy/dx viewLim ratio needs to increase
1557-
if abs(y_expander) < 0.005:
1558-
return
1531+
y_expander = data_ratio * xsize / ysize - 1
1532+
# If y_expander > 0, the dy/dx viewLim ratio needs to increase
1533+
if abs(y_expander) < 0.005:
1534+
return
15591535

1560-
if aspect_scale_mode == "log":
15611536
dL = self.dataLim
1562-
dL_width = math.log10(dL.x1) - math.log10(dL.x0)
1563-
dL_height = math.log10(dL.y1) - math.log10(dL.y0)
1564-
xr = 1.05 * dL_width
1565-
yr = 1.05 * dL_height
1566-
else:
1567-
dL = self.dataLim
1568-
xr = 1.05 * dL.width
1569-
yr = 1.05 * dL.height
1570-
1571-
xmarg = xsize - xr
1572-
ymarg = ysize - yr
1573-
Ysize = data_ratio * xsize
1574-
Xsize = ysize / data_ratio
1575-
Xmarg = Xsize - xr
1576-
Ymarg = Ysize - yr
1577-
# Setting these targets to, e.g., 0.05*xr does not seem to
1578-
# help.
1579-
xm = 0
1580-
ym = 0
1581-
1582-
shared_x = self in self._shared_x_axes
1583-
shared_y = self in self._shared_y_axes
1584-
# Not sure whether we need this check:
1585-
if shared_x and shared_y:
1586-
raise RuntimeError("adjustable='datalim' is not allowed when both"
1587-
" axes are shared.")
1588-
1589-
# If y is shared, then we are only allowed to change x, etc.
1590-
if shared_y:
1591-
adjust_y = False
1592-
else:
1593-
if xmarg > xm and ymarg > ym:
1594-
adjy = ((Ymarg > 0 and y_expander < 0) or
1595-
(Xmarg < 0 and y_expander > 0))
1537+
x0, x1 = map(x_trf.inverted().transform, dL.intervalx)
1538+
y0, y1 = map(y_trf.inverted().transform, dL.intervaly)
1539+
xr = 1.05 * (x1 - x0)
1540+
yr = 1.05 * (y1 - y0)
1541+
1542+
xmarg = xsize - xr
1543+
ymarg = ysize - yr
1544+
Ysize = data_ratio * xsize
1545+
Xsize = ysize / data_ratio
1546+
Xmarg = Xsize - xr
1547+
Ymarg = Ysize - yr
1548+
# Setting these targets to, e.g., 0.05*xr does not seem to help.
1549+
xm = 0
1550+
ym = 0
1551+
1552+
shared_x = self in self._shared_x_axes
1553+
shared_y = self in self._shared_y_axes
1554+
# Not sure whether we need this check:
1555+
if shared_x and shared_y:
1556+
raise RuntimeError("adjustable='datalim' is not allowed when "
1557+
"both axes are shared")
1558+
1559+
# If y is shared, then we are only allowed to change x, etc.
1560+
if shared_y:
1561+
adjust_y = False
15961562
else:
1597-
adjy = y_expander > 0
1598-
adjust_y = shared_x or adjy # (Ymarg > xmarg)
1599-
1600-
if adjust_y:
1601-
yc = 0.5 * (ymin + ymax)
1602-
y0 = yc - Ysize / 2.0
1603-
y1 = yc + Ysize / 2.0
1604-
if aspect_scale_mode == "log":
1605-
self.set_ybound((10. ** y0, 10. ** y1))
1606-
else:
1607-
self.set_ybound((y0, y1))
1608-
else:
1609-
xc = 0.5 * (xmin + xmax)
1610-
x0 = xc - Xsize / 2.0
1611-
x1 = xc + Xsize / 2.0
1612-
if aspect_scale_mode == "log":
1613-
self.set_xbound((10. ** x0, 10. ** x1))
1563+
if xmarg > xm and ymarg > ym:
1564+
adjy = ((Ymarg > 0 and y_expander < 0) or
1565+
(Xmarg < 0 and y_expander > 0))
1566+
else:
1567+
adjy = y_expander > 0
1568+
adjust_y = shared_x or adjy # (Ymarg > xmarg)
1569+
1570+
if adjust_y:
1571+
yc = 0.5 * (ymin + ymax)
1572+
y0 = yc - Ysize / 2.0
1573+
y1 = yc + Ysize / 2.0
1574+
self.set_ybound(*map(y_trf.inverted().transform, (y0, y1)))
16141575
else:
1615-
self.set_xbound((x0, x1))
1576+
xc = 0.5 * (xmin + xmax)
1577+
x0 = xc - Xsize / 2.0
1578+
x1 = xc + Xsize / 2.0
1579+
self.set_xbound(*map(x_trf.inverted().transform, (x0, x1)))
16161580

16171581
def axis(self, *args, emit=True, **kwargs):
16181582
"""

lib/matplotlib/tests/test_axes.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6439,3 +6439,29 @@ def test_set_ticks_inverted():
64396439
ax.invert_xaxis()
64406440
ax.set_xticks([.3, .7])
64416441
assert ax.get_xlim() == (1, 0)
6442+
6443+
6444+
def test_aspect_nonlinear_adjustable_box():
6445+
fig = plt.figure(figsize=(10, 10)) # Square.
6446+
6447+
ax = fig.add_subplot()
6448+
ax.plot([.4, .6], [.4, .6]) # Set minpos to keep logit happy.
6449+
ax.set(xscale="log", xlim=(1, 10),
6450+
yscale="logit", ylim=(1/11, 1/1001),
6451+
aspect=1, adjustable="box")
6452+
ax.margins(0)
6453+
pos = fig.transFigure.transform_bbox(ax.get_position())
6454+
assert pos.height / pos.width == pytest.approx(2)
6455+
6456+
6457+
def test_aspect_nonlinear_adjustable_datalim():
6458+
fig = plt.figure(figsize=(10, 10)) # Square.
6459+
6460+
ax = fig.add_axes([.1, .1, .8, .8]) # Square.
6461+
ax.plot([.4, .6], [.4, .6]) # Set minpos to keep logit happy.
6462+
ax.set(xscale="log", xlim=(1, 10),
6463+
yscale="logit", ylim=(1/11, 1/1001),
6464+
aspect=1, adjustable="datalim")
6465+
ax.margins(0)
6466+
ax.apply_aspect()
6467+
assert ax.get_xlim() == pytest.approx(np.array([1/10, 10]) * np.sqrt(10))

0 commit comments

Comments
 (0)
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