Skip to content

Commit b15fb4e

Browse files
committed
Rewrite axline with custom Line2D subclass.
This makes it also work on non-linear scales. Also don't use add_line directly but manually copy most of add_line, to ensure data limits are correctly set.
1 parent f967c8b commit b15fb4e

File tree

6 files changed

+61
-37
lines changed

6 files changed

+61
-37
lines changed

doc/api/axes_api.rst

+1
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ Spans
8787
Axes.axhspan
8888
Axes.axvline
8989
Axes.axvspan
90+
Axes.axline
9091

9192
Spectral
9293
--------
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
New `axline` method
2-
-------------------
1+
New `~.axes.Axes.axline` method
2+
-------------------------------
33

44
A new `~.axes.Axes.axline` method has been added to draw infinitely long lines
55
that pass through two points.

lib/matplotlib/axes/_axes.py

+13-34
Original file line numberDiff line numberDiff line change
@@ -948,50 +948,29 @@ def axline(self, xy1, xy2, **kwargs):
948948
949949
>>> axline((0, 0), (1, 1), linewidth=4, color='r')
950950
951-
952951
See Also
953952
--------
954953
axhline : for horizontal lines
955954
axvline : for vertical lines
956-
957-
Notes
958-
-----
959-
Currently this method does not work properly with non-linear axes.
960955
"""
961-
if not self.get_xscale() == self.get_yscale() == 'linear':
962-
raise NotImplementedError('axline() is only supported on '
963-
'linearly scaled axes')
964956

965957
if "transform" in kwargs:
966958
raise TypeError("'transform' is not allowed as a kwarg; "
967-
"axline generates its own transform.")
968-
959+
"axline generates its own transform")
969960
x1, y1 = xy1
970961
x2, y2 = xy2
971-
# If x values the same, we have a vertical line
972-
if np.allclose(x1, x2):
973-
if np.allclose(y1, y2):
974-
raise ValueError(
975-
'Cannot draw a line through two identical points '
976-
f'(got x1={x1}, x2={x2}, y1={y1}, y2={y2}).')
977-
line = self.axvline(x1, **kwargs)
978-
return line
979-
980-
slope = (y2 - y1) / (x2 - x1)
981-
intercept = y1 - (slope * x1)
982-
983-
xtrans = mtransforms.BboxTransformTo(self.viewLim)
984-
viewLimT = mtransforms.TransformedBbox(
985-
self.viewLim,
986-
mtransforms.Affine2D().rotate_deg(90).scale(-1, 1))
987-
ytrans = (mtransforms.BboxTransformTo(viewLimT) +
988-
mtransforms.Affine2D().scale(slope).translate(0, intercept))
989-
trans = mtransforms.blended_transform_factory(xtrans, ytrans)
990-
991-
line = mlines.Line2D([0, 1], [0, 1],
992-
transform=trans + self.transData,
993-
**kwargs)
994-
self.add_line(line)
962+
line = mlines._AxLine([x1, x2], [y1, y2], **kwargs)
963+
# Like add_line, but correctly handling data limits.
964+
self._set_artist_props(line)
965+
if line.get_clip_path() is None:
966+
line.set_clip_path(self.patch)
967+
if not line.get_label():
968+
line.set_label(f"_line{len(self.lines)}")
969+
self.lines.append(line)
970+
line._remove_method = self.lines.remove
971+
self.update_datalim([xy1, xy2])
972+
973+
self._request_autoscale_view()
995974
return line
996975

997976
@docstring.dedent_interpd

lib/matplotlib/lines.py

+38-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@
1515
_to_unmasked_float_array, ls_mapper, ls_mapper_r, STEP_LOOKUP_MAP)
1616
from .markers import MarkerStyle
1717
from .path import Path
18-
from .transforms import Bbox, TransformedPath
18+
from .transforms import (
19+
Affine2D, Bbox, BboxTransformFrom, BboxTransformTo, TransformedPath)
1920

2021
# Imported here for backward compatibility, even though they don't
2122
# really belong.
@@ -1448,6 +1449,42 @@ def is_dashed(self):
14481449
return self._linestyle in ('--', '-.', ':')
14491450

14501451

1452+
class _AxLine(Line2D):
1453+
def get_transform(self):
1454+
ax = self.axes
1455+
(x1, y1), (x2, y2) = ax.transScale.transform([*zip(*self.get_data())])
1456+
dx = x2 - x1
1457+
dy = y2 - y1
1458+
if np.allclose(x1, x2):
1459+
if np.allclose(y1, y2):
1460+
raise ValueError(
1461+
f"Cannot draw a line through two identical points "
1462+
f"(x={self.get_xdata()}, y={self.get_ydata()})")
1463+
# First send y1 to 0 and y2 to 1.
1464+
return (Affine2D.from_values(1, 0, 0, 1 / dy, 0, -y1 / dy)
1465+
+ ax.get_xaxis_transform(which="grid"))
1466+
if np.allclose(y1, y2):
1467+
# First send x1 to 0 and x2 to 1.
1468+
return (Affine2D.from_values(1 / dx, 0, 0, 1, -x1 / dx, 0)
1469+
+ ax.get_yaxis_transform(which="grid"))
1470+
(vxlo, vylo), (vxhi, vyhi) = ax.transScale.transform(ax.viewLim)
1471+
# General case: find intersections with view limits in either
1472+
# direction, and draw between the middle two points.
1473+
_, start, stop, _ = sorted([
1474+
(vxlo, y1 + (vxlo - x1) * dy / dx),
1475+
(vxhi, y1 + (vxhi - x1) * dy / dx),
1476+
(x1 + (vylo - y1) * dx / dy, vylo),
1477+
(x1 + (vyhi - y1) * dx / dy, vyhi),
1478+
])
1479+
return (BboxTransformFrom(Bbox([*zip(*self.get_data())]))
1480+
+ BboxTransformTo(Bbox([start, stop]))
1481+
+ ax.transLimits + ax.transAxes)
1482+
1483+
def draw(self, renderer):
1484+
self._transformed_path = None # Force regen.
1485+
super().draw(renderer)
1486+
1487+
14511488
class VertexSelector:
14521489
"""
14531490
Manage the callbacks to maintain a list of selected vertices for

lib/matplotlib/pyplot.py

+6
Original file line numberDiff line numberDiff line change
@@ -2374,6 +2374,12 @@ def axis(*args, emit=True, **kwargs):
23742374
return gca().axis(*args, emit=emit, **kwargs)
23752375

23762376

2377+
# Autogenerated by boilerplate.py. Do not edit as changes will be lost.
2378+
@docstring.copy(Axes.axline)
2379+
def axline(xy1, xy2, **kwargs):
2380+
return gca().axline(xy1, xy2, **kwargs)
2381+
2382+
23772383
# Autogenerated by boilerplate.py. Do not edit as changes will be lost.
23782384
@docstring.copy(Axes.axvline)
23792385
def axvline(x=0, ymin=0, ymax=1, **kwargs):

tools/boilerplate.py

+1
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,7 @@ def boilerplate_gen():
194194
'axhline',
195195
'axhspan',
196196
'axis',
197+
'axline',
197198
'axvline',
198199
'axvspan',
199200
'bar',

0 commit comments

Comments
 (0)