Description
Problem
The axline
function allows to plot an infinite diagonal line by specifying two points or a point and a slope in data coordinates.
There are use cases for this function where one might rather want to specify a point in axes coordinates, but keep the slope in data coordinates - e.g. when you would like to create a grid of diagonal lines filling the current viewport to highlight a diagonal feature in the plotted data.
It would probably be intuitive to allow to pass a transform
argument to the axline
function to accomplish this, but this raises a ValueError: 'transform' is not allowed as a kwarg; axvline generates its own transform.
Proposed Solution
One could solve this either with a separate function for this use case or simply allow to pass the transform
parameter to the axline
function and use the provided transformation for the point, but not for the slope.
Additional context and prior art
I have implemented a proof-of-concept solution here, based on the implementation of axline
:
def plot_diagonal(ax: Axes, xy1: tuple, slope: float, **kwargs):
"""
Plots a diagonal line specified by one point and a slope.
This function is similar to :meth:`~matplotlib.axes.Axes.axline`, but with the difference that the point is
specified in axes coordinates instead of data coordinates (while the slope is still in data coordinates). That means
that the lines stay at the same position when panning the axis.
:param ax: Axes
:param xy1: position of one point on the line, in axes coordinates
:param slope: slope of the line, in data coordinates
:param kwargs: Additional arguments to pass to the :class:`~matplotlib.lines.Line2D` constructor
:return: :class:`~matplotlib.lines.Line2D` instance
"""
class DiagonalLine(Line2D):
def __init__(self, xy1, slope, **kwargs):
super().__init__([0, 1], [0, 1], **kwargs)
self._slope = slope
self._xy1 = xy1
def get_transform(self):
ax = self.axes
(vxlo, vylo), (vxhi, vyhi) = ax.transScale.transform(ax.viewLim)
(x, y), = (ax.transAxes + ax.transData.inverted()).transform([self._xy1])
# General case: find intersections with view limits in either
# direction, and draw between the middle two points.
_, start, stop, _ = sorted([
(vxlo, y + (vxlo - x) * slope),
(vxhi, y + (vxhi - x) * slope),
(x + (vylo - y) / slope, vylo),
(x + (vyhi - y) / slope, vyhi),
])
return BboxTransformTo(Bbox([start, stop])) + ax.transLimits + ax.transAxes
def draw(self, renderer):
self._transformed_path = None # Force regen.
super().draw(renderer)
line = DiagonalLine(xy1, slope, **kwargs)
ax.add_line(line)
return line
# example:
fig, ax = plt.subplots()
slope = 0.5
for pos in np.linspace(-2, 1, 10):
# plot diagonal lines
plot_diagonal(ax, (pos, 0), slope, color='k')
plt.show()