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

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)