76

Matplotlib Axes have the functions axhline and axvline for drawing horizontal or vertical lines at a given y or x coordinate (respectively) independently of the data scale on an Axes.

Is there a similar function for plotting a constant diagonal? For example, if I have a scatterplot of variables with a similar domain, it is often useful to know whether they fall above or below the line of y = x:

mean, cov = [0, 0], [(1, .6), (.6, 1)]
x, y = np.random.multivariate_normal(mean, cov, 100).T
y += x + 1
f, ax = plt.subplots(figsize=(6, 6))
ax.scatter(x, y, c=".3")
ax.plot([-3, 3], [-3, 3], ls="--", c=".3")
ax.set(xlim=(-3, 3), ylim=(-3, 3))

enter image description here

This can of course be done programmatically by grabbing the axis limits, (ax.get_xlim(), etc.), but that a) takes a few extra steps and b) is brittle in cases where more data might end up on the plot and shift the limits. (Actually in some cases just adding the constant line itself stretches the axes).

It would be preferable to just do, e.g., ax.axdline(ls="--", c=".3"), but it's not clear if something like this exists in the matplotlib codebase. All you would need to do would be modify the axhline code to plot from [0, 1] in axes coordinates for both x and y, I think.

thefourtheye
  • 233,700
  • 52
  • 457
  • 497
mwaskom
  • 46,693
  • 16
  • 125
  • 127
  • See http://stackoverflow.com/a/14348481/6605826 . I think that might be what you want. – EL_DON Nov 23 '16 at 18:51
  • Matplotlib will likely have this in 3.3.0: https://github.com/matplotlib/matplotlib/pull/15330 – Eric Jan 16 '20 at 13:00

6 Answers6

58

Drawing a diagonal from the lower left to the upper right corners of your plot would be accomplished by the following

ax.plot([0, 1], [0, 1], transform=ax.transAxes)

Using transform=ax.transAxes, the supplied x and y coordinates are interpreted as axes coordinates instead of data coordinates.

This, as @fqq pointed out, is only the identity line when your x and y limits are equal. To draw the line y=x such that it always extends to the limits of your plot, an approach similar to the one given by @Ffisegydd would work, and can be written as the following function.

def add_identity(axes, *line_args, **line_kwargs):
    identity, = axes.plot([], [], *line_args, **line_kwargs)
    def callback(axes):
        low_x, high_x = axes.get_xlim()
        low_y, high_y = axes.get_ylim()
        low = max(low_x, low_y)
        high = min(high_x, high_y)
        identity.set_data([low, high], [low, high])
    callback(axes)
    axes.callbacks.connect('xlim_changed', callback)
    axes.callbacks.connect('ylim_changed', callback)
    return axes

Example usage:

import numpy as np
import matplotlib.pyplot as plt

mean, cov = [0, 0], [(1, .6), (.6, 1)]
x, y = np.random.multivariate_normal(mean, cov, 100).T
y += x + 1

f, ax = plt.subplots(figsize=(6, 6))
ax.scatter(x, y, c=".3")
add_identity(ax, color='r', ls='--')

plt.show()
JaminSore
  • 3,758
  • 1
  • 25
  • 21
  • 7
    This does not draw the y=x line, which is what the OP asked. – fqq Nov 05 '15 at 16:14
  • 3
    That is correct, it draws a line from the lower left to the upper right of the plot as stated in my answer. I was debating whether or not to include the caveat that you still have to ensure that the x and y limits of the plot are equal. I think it's generally good practice to set equal x and y limits if you're plotting an identity line. – JaminSore Nov 06 '15 at 03:54
  • 1
    Thanks for elaborating. There is another thing that shows up in all of these - even with your callback solution. It's that adding the diagonal line, will change the automatic axis limits. For this reason, I prefer to access the xlim/ylim, draw the line, restore xlim/ylim! – creanion Oct 30 '21 at 12:11
  • Be careful, this does not draw a Y=X line with a slope of 1. To draw X=Y line without boundries, use `plt.axline((0,0), slope=1)` – JZ1 Jun 13 '23 at 07:36
  • @JZ1 please let me know if there's an edge case that the `add_identity` callback solution does not cover. I have not encountered one in the nearly eight years since posting this, and am genuinely curious if you've encountered one. As an aside, yes `plt.axline` is now the preferred solution :) . – JaminSore Jun 22 '23 at 20:20
37

Plotting a diagonal line based from the bottom-left to the top-right of the screen is quite simple, you can simply use ax.plot(ax.get_xlim(), ax.get_ylim(), ls="--", c=".3"). The method ax.get_xlim() will simply return the current values of the x-axis (and similarly for the y-axis).

However, if you want to be able to zoom using your graph then it becomes slightly more tricky, as the diagonal line that you have plotted will not change to match your new xlims and ylims.

In this case you can use callbacks to check when the xlims (or ylims) have changed and change the data in your diagonal line accordingly (as shown below). I found the methods for callbacks in this example. Further information can also be found here

import numpy as np
import matplotlib.pyplot as plt

mean, cov = [0, 0], [(1, .6), (.6, 1)]
x, y = np.random.multivariate_normal(mean, cov, 100).T
y += x + 1

f, ax = plt.subplots(figsize=(6, 6))

ax.scatter(x, y, c=".3")
ax.set(xlim=(-3, 3), ylim=(-3, 3))

# Plot your initial diagonal line based on the starting
# xlims and ylims.
diag_line, = ax.plot(ax.get_xlim(), ax.get_ylim(), ls="--", c=".3")

def on_change(axes):
    # When this function is called it checks the current
    # values of xlim and ylim and modifies diag_line
    # accordingly.
    x_lims = ax.get_xlim()
    y_lims = ax.get_ylim()
    diag_line.set_data(x_lims, y_lims)

# Connect two callbacks to your axis instance.
# These will call the function "on_change" whenever
# xlim or ylim is changed.
ax.callbacks.connect('xlim_changed', on_change)
ax.callbacks.connect('ylim_changed', on_change)

plt.show()

Note that if you don't want the diagonal line to change with zooming then you simply remove everything below diag_line, = ax.plot(...

Ffisegydd
  • 51,807
  • 15
  • 147
  • 125
  • 1
    Sorry, I understand how to do this programmatically, I'm just wondering if there is something in matplotlib that saves the trouble of getting the limits, etc., which is brittle because they may change. I'm not worried so much about interactive zooming, just in general the data limits changing as data is added to the plot, or if you wanted to add a diagonal line independent of any other data you're going to plot. I'll edit my question to make this more clear. – mwaskom Feb 28 '14 at 21:06
  • Just so you're aware the diagonal line will not just change on interactive zoom but also when you add data to the plot and change it via, for e.g., `ax.set_xlim(a, b)`. I don't understand what you mean by "brittle because they may change."? The whole idea of my answer is that whenever they do change the diagonal line will be modified to take that into account. – Ffisegydd Feb 28 '14 at 21:22
  • Gotcha. The callback tip is useful. I meant *just* using `get_xlim()` and `get_ylim()` at the initial draw time. – mwaskom Feb 28 '14 at 21:36
33

Starting from matplotlib 3.3.0, it will: https://matplotlib.org/3.3.0/api/_as_gen/matplotlib.axes.Axes.axline.html

Axes.axline(self, xy1, xy2=None, *, slope=None, **kwargs) Add an infinitely long straight line.

The line can be defined either by two points xy1 and xy2, or by one point xy1 and a slope.

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. However, slope should only be used with linear scales; It has no clear meaning for all other scales, and thus the behavior is undefined. Please specify the line using the points xy1, xy2 for non-linear scales.

mwaskom
  • 46,693
  • 16
  • 125
  • 127
  • 7
    Indeed, `ax.axline((1, 1), slope=1)` should do it – Emanuel Jul 20 '20 at 10:00
  • 5
    For me this does not work. I get an attributeError: 'AxesSubplot' object has not attribute 'axline'. And if I try it in a bit different set up I get: AttributeError: moule 'matplotlib.pyplot' has no attribute 'axline. I use hline and vline in both these settings without problems. – Linda Aug 03 '20 at 12:16
  • This is clearly superior to the more upvoted answers if your matplotlib is sufficiently recent. Thank you! – Blaise Dec 04 '21 at 08:04
  • For anyone having the same problem as @Linda: update your matplotlib version to at least 3.3.0. – Peiffap Nov 02 '22 at 14:13
  • 1
    If you get the following error message: `TypeError: 'slope' cannot be used with non-linear scales`, it is probably because you have scaled at least one axis of your plot with e.g. a logarithmic scale. To avoid this error just create the diagonal line before changing the scale of the plot axes and it works correctly – Miguel Dec 08 '22 at 10:34
12

If the axes are in the range [0,1], it can be resolved in this way:

ident = [0.0, 1.0]
plt.plot(ident,ident)
volperossa
  • 1,339
  • 20
  • 33
2

This will always work as it dynamically adjusts to the axis scales

ax.axline([ax.get_xlim()[0], ax.get_ylim()[0]], [ax.get_xlim()[1], ax.get_ylim()[1]])
ats
  • 141
  • 6
0

According to https://matplotlib.org/stable/gallery/pyplots/axline.html , you can use the plt.axline. And example from the documentation:

import matplotlib.pyplot as plt

t = np.linspace(-10, 10, 100)
sig = 1 / (1 + np.exp(-t))

plt.axhline(y=0, color="black", linestyle="--")
plt.axhline(y=0.5, color="black", linestyle=":")
plt.axhline(y=1.0, color="black", linestyle="--")
plt.axvline(color="grey")
plt.axline((0, 0.5), slope=0.25, color="black", linestyle=(0, (5, 5)))
plt.plot(t, sig, linewidth=2, label=r"$\sigma(t) = \frac{1}{1 + e^{-t}}$")
plt.xlim(-10, 10)
plt.xlabel("t")
plt.legend(fontsize=14)
plt.show()```
Fariborz Ghavamian
  • 809
  • 2
  • 11
  • 23