Skip to content

Add axes method for drawing infinite lines. #15330

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Oct 23, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions doc/api/axes_api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ Spans
Axes.axhspan
Axes.axvline
Axes.axvspan
Axes.axline

Spectral
--------
Expand Down
5 changes: 5 additions & 0 deletions doc/users/next_whats_new/2017-12-08-axline.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
New `~.axes.Axes.axline` method
-------------------------------

A new `~.axes.Axes.axline` method has been added to draw infinitely long lines
that pass through two points.
41 changes: 21 additions & 20 deletions examples/subplots_axes_and_figures/axhspan_demo.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,32 +4,33 @@
============

Create lines or rectangles that span the axes in either the horizontal or
vertical direction.
vertical direction, and lines than span the axes with an arbitrary orientation.
"""

import numpy as np
import matplotlib.pyplot as plt

t = np.arange(-1, 2, .01)
s = np.sin(2 * np.pi * t)

plt.plot(t, s)
# Draw a thick red hline at y=0 that spans the xrange
plt.axhline(linewidth=8, color='#d62728')

# Draw a default hline at y=1 that spans the xrange
plt.axhline(y=1)

# Draw a default vline at x=1 that spans the yrange
plt.axvline(x=1)

# Draw a thick blue vline at x=0 that spans the upper quadrant of the yrange
plt.axvline(x=0, ymin=0.75, linewidth=8, color='#1f77b4')

# Draw a default hline at y=.5 that spans the middle half of the axes
plt.axhline(y=.5, xmin=0.25, xmax=0.75)

plt.axhspan(0.25, 0.75, facecolor='0.5', alpha=0.5)

plt.axvspan(1.25, 1.55, facecolor='#2ca02c', alpha=0.5)
fig, ax = plt.subplots()

ax.plot(t, s)
# Thick red horizontal line at y=0 that spans the xrange.
ax.axhline(linewidth=8, color='#d62728')
# Horizontal line at y=1 that spans the xrange.
ax.axhline(y=1)
# Vertical line at x=1 that spans the yrange.
ax.axvline(x=1)
# Thick blue vertical line at x=0 that spans the upper quadrant of the yrange.
ax.axvline(x=0, ymin=0.75, linewidth=8, color='#1f77b4')
# Default hline at y=.5 that spans the middle half of the axes.
ax.axhline(y=.5, xmin=0.25, xmax=0.75)
# Infinite black line going through (0, 0) to (1, 1).
ax.axline((0, 0), (1, 1), color='k')
# 50%-gray rectangle spanning the axes' width from y=0.25 to y=0.75.
ax.axhspan(0.25, 0.75, facecolor='0.5')
# Green rectangle spanning the axes' height from x=1.25 to x=1.55.
ax.axvspan(1.25, 1.55, facecolor='#2ca02c')

plt.show()
59 changes: 59 additions & 0 deletions lib/matplotlib/axes/_axes.py
Original file line number Diff line number Diff line change
Expand Up @@ -816,6 +816,7 @@ def axhline(self, y=0, xmin=0, xmax=1, **kwargs):
--------
hlines : Add horizontal lines in data coordinates.
axhspan : Add a horizontal span (rectangle) across the axis.
axline : Add a line with an arbitrary slope.

Examples
--------
Expand Down Expand Up @@ -899,6 +900,7 @@ def axvline(self, x=0, ymin=0, ymax=1, **kwargs):
--------
vlines : Add vertical lines in data coordinates.
axvspan : Add a vertical span (rectangle) across the axis.
axline : Add a line with an abritrary slope.
"""

if "transform" in kwargs:
Expand All @@ -919,6 +921,63 @@ def axvline(self, x=0, ymin=0, ymax=1, **kwargs):
self._request_autoscale_view(scalex=scalex, scaley=False)
return l

@docstring.dedent_interpd
def axline(self, xy1, xy2, **kwargs):
"""
Add an infinitely long straight line that passes through two points.

This draws a straight line "on the screen", regardless of the x and y
scales, and is thus also suitable for drawing exponential decays in
semilog plots, power laws in loglog plots, etc.

Parameters
----------
xy1, xy2 : (float, float)
Points for the line to pass through.

Returns
-------
:class:`~matplotlib.lines.Line2D`

Other Parameters
----------------
**kwargs
Valid kwargs are :class:`~matplotlib.lines.Line2D` properties,
with the exception of 'transform':

%(_Line2D_docstr)s

Examples
--------
Draw a thick red line passing through (0, 0) and (1, 1)::

>>> axline((0, 0), (1, 1), linewidth=4, color='r')
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add an example in log space...

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

actually this example works fine (but demonstrates vlines/hlines which behave stupidly differently from axvline/axhline) -- the example that needs to be modified is https://matplotlib.org/gallery/subplots_axes_and_figures/axhspan_demo.html.


See Also
--------
axhline : for horizontal lines
axvline : for vertical lines
"""

if "transform" in kwargs:
raise TypeError("'transform' is not allowed as a kwarg; "
"axline generates its own transform")
x1, y1 = xy1
x2, y2 = xy2
line = mlines._AxLine([x1, x2], [y1, y2], **kwargs)
# Like add_line, but correctly handling data limits.
self._set_artist_props(line)
if line.get_clip_path() is None:
line.set_clip_path(self.patch)
if not line.get_label():
line.set_label(f"_line{len(self.lines)}")
self.lines.append(line)
line._remove_method = self.lines.remove
self.update_datalim([xy1, xy2])
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might be nice to provide a way to exclude this

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To elaborate on this comment:

I've come across two use-cases where this scaling behavior is undesirable:

  • I'm plotting a line with a single special point (eg, a ray, and I'm ok with the backwards portion of the line). If I plot (origin, origin + direction), then my axis bounds scale to meet origin + direction, even though that point is arbitrary. I can work around that with a hack like (origin, origin + eps*direction), but it forces me to trade off axis scaling against precision.

  • I'm plotting a true infinite line, with no special points. Perhaps ideally I'd get axis scaling that shows the "nearest" part of the line, but I'd be ok with just passing autoscale=False to axline to have it skip this step.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree that it should be possible to skip the autoscaling. The two points being specified only need to be somewhere along the desired infinite line, and it's perfectly reasonable that they might not be within the domain one actually wants to view. I'm actually wondering whether it would make more sense to leave out the update_datalim entirely instead of adding a kwarg; but maybe it's too late for that. In any case, please open a new issue about this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's move this discussion to #16264 (which I replied to already).


self._request_autoscale_view()
return line

@docstring.dedent_interpd
def axhspan(self, ymin, ymax, xmin=0, xmax=1, **kwargs):
"""
Expand Down
44 changes: 43 additions & 1 deletion lib/matplotlib/lines.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@
_to_unmasked_float_array, ls_mapper, ls_mapper_r, STEP_LOOKUP_MAP)
from .markers import MarkerStyle
from .path import Path
from .transforms import Bbox, TransformedPath
from .transforms import (
Affine2D, Bbox, BboxTransformFrom, BboxTransformTo, TransformedPath)

# Imported here for backward compatibility, even though they don't
# really belong.
Expand Down Expand Up @@ -1448,6 +1449,47 @@ def is_dashed(self):
return self._linestyle in ('--', '-.', ':')


class _AxLine(Line2D):
"""
A helper class that implements `~.Axes.axline`, by recomputing the artist
transform at draw time.
"""

def get_transform(self):
ax = self.axes
(x1, y1), (x2, y2) = ax.transScale.transform([*zip(*self.get_data())])
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={self.get_xdata()}, y={self.get_ydata()})")
# First send y1 to 0 and y2 to 1.
return (Affine2D.from_values(1, 0, 0, 1 / dy, 0, -y1 / dy)
+ ax.get_xaxis_transform(which="grid"))
if np.allclose(y1, y2):
# First send x1 to 0 and x2 to 1.
return (Affine2D.from_values(1 / dx, 0, 0, 1, -x1 / dx, 0)
+ ax.get_yaxis_transform(which="grid"))
(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.
_, start, stop, _ = sorted([
(vxlo, y1 + (vxlo - x1) * dy / dx),
(vxhi, y1 + (vxhi - x1) * dy / dx),
(x1 + (vylo - y1) * dx / dy, vylo),
(x1 + (vyhi - y1) * dx / dy, vyhi),
])
return (BboxTransformFrom(Bbox([*zip(*self.get_data())]))
+ BboxTransformTo(Bbox([start, stop]))
+ ax.transLimits + ax.transAxes)

def draw(self, renderer):
self._transformed_path = None # Force regen.
super().draw(renderer)


class VertexSelector:
"""
Manage the callbacks to maintain a list of selected vertices for
Expand Down
6 changes: 6 additions & 0 deletions lib/matplotlib/pyplot.py
Original file line number Diff line number Diff line change
Expand Up @@ -2371,6 +2371,12 @@ def axis(*args, emit=True, **kwargs):
return gca().axis(*args, emit=emit, **kwargs)


# Autogenerated by boilerplate.py. Do not edit as changes will be lost.
@docstring.copy(Axes.axline)
def axline(xy1, xy2, **kwargs):
return gca().axline(xy1, xy2, **kwargs)


# Autogenerated by boilerplate.py. Do not edit as changes will be lost.
@docstring.copy(Axes.axvline)
def axvline(x=0, ymin=0, ymax=1, **kwargs):
Expand Down
15 changes: 15 additions & 0 deletions lib/matplotlib/tests/test_axes.py
Original file line number Diff line number Diff line change
Expand Up @@ -3641,6 +3641,21 @@ def test_eb_line_zorder():
ax.set_title("errorbar zorder test")


@check_figures_equal()
def test_axline(fig_test, fig_ref):
ax = fig_test.subplots()
ax.set(xlim=(-1, 1), ylim=(-1, 1))
ax.axline((0, 0), (1, 1))
ax.axline((0, 0), (1, 0), color='C1')
ax.axline((0, 0.5), (1, 0.5), color='C2')

ax = fig_ref.subplots()
ax.set(xlim=(-1, 1), ylim=(-1, 1))
ax.plot([-1, 1], [-1, 1])
ax.axhline(0, color='C1')
ax.axhline(0.5, color='C2')


@image_comparison(['vlines_basic', 'vlines_with_nan', 'vlines_masked'],
extensions=['png'])
def test_vlines():
Expand Down
1 change: 1 addition & 0 deletions tools/boilerplate.py
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,7 @@ def boilerplate_gen():
'axhline',
'axhspan',
'axis',
'axline',
'axvline',
'axvspan',
'bar',
Expand Down