Reputation: 1624
We can draw infinite lines from a given point with given slope in matplotlib with plt.axline() (https://matplotlib.org/stable/api/_as_gen/matplotlib.pyplot.axline.html)
Is there a clean way to draw a semi-infinite line or ray from a given point in a given direction? Preferably without having to calculate the axis limits.
For axhline and axvline, we can use one of xmin, xmax, ymin, ymax arguments to get a ray, but axline doesn't accept these.
Related questions:
Upvotes: 6
Views: 2229
Reputation: 51
Here is a modifications of axline which allows to specify a semi_x
argument. It controls which x-halfplane around the xy1
point to draw.
This works with both slope
and xy2
arguments. Ignoring semi_x
preserves the default axline
behaviour.
import matplotlib.pyplot as plt
import numpy as np
from matplotlib.transforms import Bbox, BboxTransformTo
from matplotlib.lines import Line2D
def axline(ax, xy1, xy2=None, *, slope=None, semi_x=None, **kwargs):
if slope is not None and (ax.get_xscale() != 'linear' or
ax.get_yscale() != 'linear'):
raise TypeError("'slope' cannot be used with non-linear scales")
datalim = [xy1] if xy2 is None else [xy1, xy2]
if "transform" in kwargs:
# if a transform is passed (i.e. line points not in data space),
# data limits should not be adjusted.
datalim = []
line = _AxLine(xy1, xy2, slope, semi_x, **kwargs)
# Like add_line, but correctly handling data limits.
ax._set_artist_props(line)
if line.get_clip_path() is None:
line.set_clip_path(ax.patch)
if not line.get_label():
line.set_label(f"_line{len(ax.lines)}")
ax.lines.append(line)
line._remove_method = ax.lines.remove
ax.update_datalim(datalim)
ax._request_autoscale_view()
return line
class _AxLine(Line2D):
def __init__(self, xy1, xy2, slope, semi_x, **kwargs):
super().__init__([0, 1], [0, 1], **kwargs)
if (xy2 is None and slope is None or
xy2 is not None and slope is not None):
raise TypeError(
"Exactly one of 'xy2' and 'slope' must be given")
self._slope = slope
self._xy1 = xy1
self._xy2 = xy2
self._semi_x = semi_x
def get_transform(self):
ax = self.axes
points_transform = self._transform - ax.transData + ax.transScale
if self._xy2 is not None:
# two points were given
(x1, y1), (x2, y2) = \
points_transform.transform([self._xy1, self._xy2])
dx = x2 - x1
dy = y2 - y1
if np.allclose(x1, x2):
if np.allclose(y1, y2):
raise ValueError(
f"Cannot draw a line through two identical points "
f"(x={(x1, x2)}, y={(y1, y2)})")
slope = np.inf
else:
slope = dy / dx
else:
# one point and a slope were given
x1, y1 = points_transform.transform(self._xy1)
slope = self._slope
(vxlo, vylo), (vxhi, vyhi) = ax.transScale.transform(ax.viewLim)
# General case: find intersections with view limits in either
# direction, and draw between the middle two points.
if np.isclose(slope, 0):
start = vxlo, y1
stop = vxhi, y1
elif np.isinf(slope):
start = x1, vylo
stop = x1, vyhi
else:
_, start, stop, _ = sorted([
(vxlo, y1 + (vxlo - x1) * slope),
(vxhi, y1 + (vxhi - x1) * slope),
(x1 + (vylo - y1) / slope, vylo),
(x1 + (vyhi - y1) / slope, vyhi),
])
# Handle semi-plane
if self._semi_x == True:
start = (x1,y1)
elif self._semi_x == False:
stop = (x1,y1)
return (BboxTransformTo(Bbox([start, stop]))
+ ax.transLimits + ax.transAxes)
def draw(self, renderer):
self._transformed_path = None # Force regen.
super().draw(renderer)
## Usage with slope
fig, ax = plt.subplots()
xy1 = (.5, .5)
slope = -1
ax.scatter(*xy1)
axline(ax, xy1, slope=slope, c='g', semi_x=True)
axline(ax, xy1, slope=slope, c='r', semi_x=False)
ax.set_xlim([0,1])
ax.set_ylim([0,1])
plt.show()
## Usage with xy2
fig, ax = plt.subplots()
xy1 = (.5, .5)
xy2 = (.75, .75)
ax.scatter(*xy1)
ax.scatter(*xy2)
axline(ax, xy1, xy2=xy2, c='g', semi_x=True)
axline(ax, xy1, xy2=xy2, c='r', semi_x=False)
ax.set_xlim([0,1])
ax.set_ylim([0,1])
plt.show()
Upvotes: 5
Reputation: 19545
I can't find a clean way to do this based on the axline documentation, so I'll post my hacky workaround which is to obscure the portion of the line by drawing a line segment (with a larger linewidth
than your axline) from xmin to the x value of your starting point.
I acknowledge that this is an ugly solution and will update my answer if I think of anything better.
import matplotlib.pyplot as plt
## draw infinite line starting from (0.5,0.5) with slope=1
x0,y0,m= 0.5,0.5,1
plt.axline((x0, y0), slope=m, color='k', transform=plt.gca().transAxes, linewidth=0.5, alpha=0.5)
## obscure the line segment from xmin to x0
ymin,ymax = plt.gca().get_ylim()
xmin = x0 - (y0-ymin / m)
## plot twice so that a portion of the axline can't be seen
plt.plot([xmin,x0], [ymin,y0], '#ffffff', linewidth=1.0, alpha=1.0)
plt.plot([xmin,x0], [ymin,y0], '#ffffff', linewidth=1.0, alpha=1.0)
plt.ylim([0, 1])
plt.xlim([0, 1])
plt.show()
Upvotes: 2