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()
Example use with slope
Example use with xy2