Skip to content

Axes.axline: allow to specify position in axes coordinates instead of data coordinates #18625

Closed
@johan12345

Description

@johan12345

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()

Figure_5

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions