matplotlib legend avoids points when manually setting coordinates - python

In the following code, I generate a scatterplot where the legend is manually placed:
#!/usr/bin/env python3
import pandas as pd
import matplotlib.pyplot as plt
import numpy as np
frame = frame = pd.DataFrame({"x": [1, 2, 3, 4], "y": [4, 3, 2, 1]})
ax = frame.plot.scatter(x="x", y="y", label="dots")
plt.savefig("dots.pdf")
for y in [0.6, 0.7, 0.8]:
ax.legend(bbox_to_anchor=(0.5, y), bbox_transform=ax.transAxes)
plt.savefig("dots_{}.png".format(y))
It looks like the legend does not obey the placement instructions when it would make it hide a point:
Is there a way to avoid this? I mean, how to really force the placement of the legend?

You may be interested in reading my answer to "How to put the legend out of the plot". While it handles the case of putting the legend outside of the plot, most of it is applicable to placing the legend just anywhere, including inside the plot.
Most imporantly, the legend position is determined by the loc parameter. If you do not specify this parameter in the call to legend(), matplotlib will try to place the legend whereever it thinks it's best, (default is loc ="best").
In case you want to place the legend at a certain position, you may specify the coordinates of its lower left corner to loc:
ax.legend(loc=(0.5, 0.6))
If you want to specify another corner of the legend to be at a certain position, you need to specify the corner with the loc argument and the position using bbox_to_anchor:
ax.legend(loc="upper right", bbox_to_anchor=(0.5, 0.6))

Related

Horizontal line to infinity on one side only in matplotlib

I'd like to plot a line that goes to infinity, but starting from a finite point. For simplicity, let's say that the line can be horizontal. I would like to plot a line from (0, 0) to (inf, 0).
Using hlines:
>>> fig, ax = plt.subplots()
>>> ax.hlines(0, 0, np.inf)
.../python3.8/site-packages/matplotlib/axes/_base.py:2480: UserWarning: Warning: converting a masked element to nan.
xys = np.asarray(xys)
The result is an empty plot.
axhline has a starting parameter, but it is in axis coordinates rather than data. Similar problem for axline. Is there a way to plot a (horizontal) line with one end in data coordinates and the other at infinity?
The motivation behind this is that I'd like to be able to plot some cumulative probabilities without setting data past the last bin to zero, as here: Matplotlib cumulative histogram - vertical line placement bug or misinterpretation?. Rather than simply ending the histogram, I'd like to be able to extend the line from the last bin to infinity at y=1.0.
There's no built-in function for this, but you can re-draw the line to the axis limit on each change of the x limits.
From Axes:
The events you can connect to are 'xlim_changed' and 'ylim_changed'
and the callback will be called with func(ax) where ax is the Axes
instance.
import matplotlib.pyplot as plt
fig, ax = plt.subplots()
def hline_to_inf(ax, x, y):
line = ax.hlines(0, 0, ax.get_xlim()[1])
ax.callbacks.connect('xlim_changed',
lambda ax: line.set_paths([[[x, y], [ax.get_xlim()[1], y]]]))
hline_to_inf(ax, 0, 0)
plt.show()
Part of the issue is that normal plotting methods apply the same transform to the input data. What is required here is to apply a data transform to the start point, and a blended transform to the endpoint. It seems that there may be an answer using existing tools with ConnectionPatch, as explained in the Annotations Guide. The idea is to make the left point use data coordinates and the right point have a blended transform with x in axes coordinates and y in data.
from matplotlib import pyplot as plt
from matplotlib.patches import ConnectionPatch
fig, ax = plt.subplots()
line, = ax.plot([1, 2], [1, 2])
ax.add_artist(ConnectionPatch([2, 2], [1, 2], coordsA=ax.transData, coordsB=ax.get_yaxis_transform(), color=line.get_color(), linewidth=line.get_linewidth(), clip_on=True))
Turning on clipping is necessary, otherwise you could end up with artifacts that look like this:

How to display axis label on both sides of the figure in maptplotlib?

I want X-axis to be exactly the same both at the bottom and top of my figure. While the ticklabels can be duplicated using ax.tick_params(labeltop=True), it seems to be no analogous command for the label of the axis itself, as ax.xaxis.set_label_position('top') relocates the label instead of duplicating it.
All the similar questions that I found (e.g. In matplotlib, how do you display an axis on both sides of the figure?) seem to be only concerned about the ticklabels, not the axis labels.
I tried using ax.twiny() but it adds a black frame around my plot. Is there a cleaner, minimalist way of duplicating the axis label to the other side of the figure to complement the duplicated ticklabels?
EDIT: minimal working example of the black frame added by ax.twiny():
import seaborn as sns
from matplotlib import pyplot as plt
ax = sns.heatmap([[0, 1], [1, 0]], cbar=None) # no outline around the heatmap
ax.twiny() # adds black frame
(Not a matplotlib expert by any means, but regarding your edit):
sns.despine(left=True, bottom=True) will remove the spines.
You should be able to replicate the xticks, xticklabels, xlim, and xlabel on the twin Axes like so:
ax = sns.heatmap([[0, 1], [1, 0]], cbar=None) # no outline around the heatmap
ax.set_xlabel('foo')
ax1 = ax.twiny() # adds black frame
ax1.set_xticks(ax.get_xticks())
ax1.set_xticklabels(ax.get_xticklabels())
ax1.set_xlabel(ax.get_xlabel())
ax1.set_xlim(ax.get_xlim())
sns.despine(left=True, bottom=True) # remove spines

matplotlib legend with alpha interaction [duplicate]

I'm working on a plot with translucent 'x' markers (20% alpha). How do I make the marker appear at 100% opacity in the legend?
import matplotlib.pyplot as plt
plt.plot_date( x = xaxis, y = yaxis, marker = 'x', color=[1, 0, 0, .2], label='Data Series' )
plt.legend(loc=3, mode="expand", numpoints=1, scatterpoints=1 )
UPDATED:
There is an easier way! First, assign your legend to a variable when you create it:
leg = plt.legend()
Then:
for lh in leg.legendHandles:
lh.set_alpha(1)
OR if the above doesn't work (you may be using an older version of matplotlib):
for lh in leg.legendHandles:
lh._legmarker.set_alpha(1)
to make your markers opaque for a plt.plot or a plt.scatter, respectively.
Note that using simply lh.set_alpha(1) on a plt.plot will make the lines in your legend opaque rather than the markers. You should be able to adapt these two possibilities for the other plot types.
Sources:
Synthesized from some good advice by DrV about marker sizes. Update was inspired by useful comment from Owen.
Following up on cosmosis's answer, to make the "fake" lines for the legend invisible on the plot, you can use NaNs, and they will still work for generating legend entries:
import numpy as np
import matplotlib.pyplot as plt
# Plot data with alpha=0.2
plt.plot((0,1), (0,1), marker = 'x', color=[1, 0, 0, .2])
# Plot non-displayed NaN line for legend, leave alpha at default of 1.0
legend_line_1 = plt.plot( np.NaN, np.NaN, marker = 'x', color=[1, 0, 0], label='Data Series' )
plt.legend()
Other answers here give good practical solutions by either changing the alpha value in the legend after creation, or changing the alpha of the line after legend creation.
A solution to achieve a different opacity in the legend without manipulating anything afterwards would be the following. It uses a handler_map and an updating function.
import matplotlib.pyplot as plt
import numpy as np; np.random.seed(43)
from matplotlib.collections import PathCollection
from matplotlib.legend_handler import HandlerPathCollection, HandlerLine2D
plt.plot(np.linspace(0,1,8), np.random.rand(8), marker="o", markersize=12, label="A line", alpha=0.2)
plt.scatter(np.random.rand(8),np.random.rand(8), s=144,
c="red", marker=r"$\clubsuit$", label="A scatter", alpha=0.2)
def update(handle, orig):
handle.update_from(orig)
handle.set_alpha(1)
plt.legend(handler_map={PathCollection : HandlerPathCollection(update_func= update),
plt.Line2D : HandlerLine2D(update_func = update)})
plt.show()
If you want to have something specific in your legend, it's easier to define objects that you place in the legend with appropriate text. For example:
import matplotlib.pyplot as plt
import pylab
plt.plot_date( x = xaxis, y = yaxis, marker = 'x', color=[1, 0, 0, .2], label='Data Series' )
line1 = pylab.Line2D(range(1),range(1),color='white',marker='x',markersize=10, markerfacecolor="red",alpha=1.0)
line2 = pylab.Line2D(range(10),range(10),marker="_",linewidth=3.0,color="dodgerblue",alpha=1.0)
plt.legend((line1,line2),('Text','Other Text'),numpoints=1,loc=1)
Here, line1 defines a short, white line (so essentially invisible) with the marker 'x' in red and full opacity. As an example, line2 gives you a longer blue line with no markers visible. By creating this "lines," you are able to more easily control their properties within the legend.
It looks like matplotlib draws the plot lines after it copies the alpha level to the legend. That means that you can create the plot lines with the alpha level that you want in the legend, create the legend to copy that alpha level, then change the alpha level on the plot lines.
Here's a complete example:
import matplotlib.pyplot as plt
x = (0, 1, 2)
y = (0, 2, 1)
line, = plt.plot(x, y, 'ro', label='label') # Default alpha is 1.0.
plt.legend() # Copy alpha to legend.
line.set_alpha(0.2) # Change alpha for data points.
plt.show()
That plot looks like this when I run it with matplotlib 2.2.3 on Python 2.7.15:
I've found that the .set_alpha() function works on many legend objects, but unfortunately, many legend objects have several pieces (such as the output of errorbar()) and the .set_alpha() call will only affect one of them.
One can use .get_legend_handles_labels() and then loop through parts of the handles and .set_alpha(), but unfortunately, copy.deepcopy() does not seem to work on the list of handles, so the plot itself will be affected. The best workaround I could find was to save the original alphas, .set_alpha() to what I wanted, create the legend, then reset the plot alphas back to their original values. It would be much cleaner if I could deepcopy handles (I wouldn't have to save alpha values or reset them), but I could not do this in python2.7 (maybe this depends on what objects are in the legend).
f,ax=plt.subplots(1)
ax.plot( ... )
def legend_alpha(ax,newalpha=1.0):
#sets alpha of legends to some value
#this would be easier if deepcopy worked on handles, but it doesn't
handles,labels=ax.get_legend_handles_labels()
alphass=[None]*len(handles) #make a list to hold lists of saved alpha values
for k,handle in enumerate(handles): #loop through the legend entries
alphas=[None]*len(handle) #make a list to hold the alphas of the pieces of this legend entry
for i,h in enumerate(handle): #loop through the pieces of this legend entry (there could be a line and a marker, for example)
try: #if handle was a simple list of parts, then this will work
alphas[i]=h.get_alpha()
h.set_alpha(newalpha)
except: #if handle was a list of parts which themselves were made up of smaller subcomponents, then we must go one level deeper still.
#this was needed for the output of errorbar() and may not be needed for simpler plot objects
alph=[None]*len(h)
for j,hh in enumerate(h):
alph[j]=hh.get_alpha() #read the alpha values of the sub-components of the piece of this legend entry
hh.set_alpha(newalpha)
alphas[i]=alph #save the list of alpha values for the subcomponents of this piece of this legend entry
alphass[k]=alphas #save the list of alpha values for the pieces of this legend entry
leg=ax.legend(handles,labels) #create the legend while handles has updated alpha values
for k,handle in enumerate(handles): #loop through legend items to restore origina alphas on the plot
for i,h in enumerate(handle): #loop through pieces of this legend item to restore alpha values on the plot
try:
h.set_alpha(alphass[k][i])
except:
for j,hh in enumerate(h): #loop through sub-components of this piece of this legend item to restore alpha values
hh.set_alpha(alphass[k][i][j])
return leg
leg=legend_alpha(ax)
leg.draggable()
In my case, set_alpha(1) also modified the edgecolors, which I didn't want: I had "invisible" edges, and setting alpha to opaque made them visible in the legend. The following snippet (OOP) changes the opacity of the face without changing the border color:
leg = ax.legend()
for lh in leg.legendHandles:
fc_arr = lh.get_fc().copy()
fc_arr[:, -1] = 1 # set opacity here
lh.set_fc(fc_arr)
Note the call to .copy(), if we don't do this it will modify the opacity for the whole plot. Calling copy means we are only modifying the facecolor inside the legend box.
Alternatively, you can add this function to your library:
def opaque_legend(ax):
"""
Calls legend, and sets all the legend colors opacity to 100%.
Returns the legend handle.
"""
leg = ax.legend()
for lh in leg.legendHandles:
fc_arr = lh.get_fc().copy()
fc_arr[:, -1] = 1
lh.set_fc(fc_arr)
return leg
And then simply replace leg = ax.legend() with leg = opaque_legend(ax). Hope this helps!
Andres
Instead of messing up with the opacity of the legend, I found another way. Firstly, I create a plot line with the style I want the legend to be. Then I change the plot line style, and, miraculously, the legend style remains intact. MWE:
plt.plot(x, y, 'ro', label='label')
for lh in plt.gca().get_legend_handles_labels():
lh[0].set_alpha(new_alpha)
I'd like to explain, why it works, but I can't. Neither I'm sure that it works for all backends.
And yes, I know that the question is old. As it still appears in Google, I'll find it later and help my future self.

How can I position a single boxplot in matplotlib?

I would like to position a single boxplot at a custom position like so:
import pylab as plt
import numpy as np
a=np.random.randn(1000)
plt.boxplot(a, positions=np.array([2.]))
but it always appears at 1. Note that plt.violinplot(a, positions=np.array([2.])) works as expected.
I believe it is plotted at the correct position, it's just that the label on the axis is still set to 1. You can see this if you try to plot something else on the axes. For instance, if you do pyplot.plot([1, 2, 3], [3, 0, -3]) you will see that the middle of line crosses through the middle of the boxplot.
This means things will show up in the right places if you're plotting other stuff on the same axes as the boxplot. If you're not plotting anything else on the same axes, it doesn't really matter where the boxes are actually located; you can just set the labels directly by using the labels argument to boxplot.

Set legend symbol opacity with matplotlib?

I'm working on a plot with translucent 'x' markers (20% alpha). How do I make the marker appear at 100% opacity in the legend?
import matplotlib.pyplot as plt
plt.plot_date( x = xaxis, y = yaxis, marker = 'x', color=[1, 0, 0, .2], label='Data Series' )
plt.legend(loc=3, mode="expand", numpoints=1, scatterpoints=1 )
UPDATED:
There is an easier way! First, assign your legend to a variable when you create it:
leg = plt.legend()
Then:
for lh in leg.legendHandles:
lh.set_alpha(1)
OR if the above doesn't work (you may be using an older version of matplotlib):
for lh in leg.legendHandles:
lh._legmarker.set_alpha(1)
to make your markers opaque for a plt.plot or a plt.scatter, respectively.
Note that using simply lh.set_alpha(1) on a plt.plot will make the lines in your legend opaque rather than the markers. You should be able to adapt these two possibilities for the other plot types.
Sources:
Synthesized from some good advice by DrV about marker sizes. Update was inspired by useful comment from Owen.
Following up on cosmosis's answer, to make the "fake" lines for the legend invisible on the plot, you can use NaNs, and they will still work for generating legend entries:
import numpy as np
import matplotlib.pyplot as plt
# Plot data with alpha=0.2
plt.plot((0,1), (0,1), marker = 'x', color=[1, 0, 0, .2])
# Plot non-displayed NaN line for legend, leave alpha at default of 1.0
legend_line_1 = plt.plot( np.NaN, np.NaN, marker = 'x', color=[1, 0, 0], label='Data Series' )
plt.legend()
Other answers here give good practical solutions by either changing the alpha value in the legend after creation, or changing the alpha of the line after legend creation.
A solution to achieve a different opacity in the legend without manipulating anything afterwards would be the following. It uses a handler_map and an updating function.
import matplotlib.pyplot as plt
import numpy as np; np.random.seed(43)
from matplotlib.collections import PathCollection
from matplotlib.legend_handler import HandlerPathCollection, HandlerLine2D
plt.plot(np.linspace(0,1,8), np.random.rand(8), marker="o", markersize=12, label="A line", alpha=0.2)
plt.scatter(np.random.rand(8),np.random.rand(8), s=144,
c="red", marker=r"$\clubsuit$", label="A scatter", alpha=0.2)
def update(handle, orig):
handle.update_from(orig)
handle.set_alpha(1)
plt.legend(handler_map={PathCollection : HandlerPathCollection(update_func= update),
plt.Line2D : HandlerLine2D(update_func = update)})
plt.show()
If you want to have something specific in your legend, it's easier to define objects that you place in the legend with appropriate text. For example:
import matplotlib.pyplot as plt
import pylab
plt.plot_date( x = xaxis, y = yaxis, marker = 'x', color=[1, 0, 0, .2], label='Data Series' )
line1 = pylab.Line2D(range(1),range(1),color='white',marker='x',markersize=10, markerfacecolor="red",alpha=1.0)
line2 = pylab.Line2D(range(10),range(10),marker="_",linewidth=3.0,color="dodgerblue",alpha=1.0)
plt.legend((line1,line2),('Text','Other Text'),numpoints=1,loc=1)
Here, line1 defines a short, white line (so essentially invisible) with the marker 'x' in red and full opacity. As an example, line2 gives you a longer blue line with no markers visible. By creating this "lines," you are able to more easily control their properties within the legend.
It looks like matplotlib draws the plot lines after it copies the alpha level to the legend. That means that you can create the plot lines with the alpha level that you want in the legend, create the legend to copy that alpha level, then change the alpha level on the plot lines.
Here's a complete example:
import matplotlib.pyplot as plt
x = (0, 1, 2)
y = (0, 2, 1)
line, = plt.plot(x, y, 'ro', label='label') # Default alpha is 1.0.
plt.legend() # Copy alpha to legend.
line.set_alpha(0.2) # Change alpha for data points.
plt.show()
That plot looks like this when I run it with matplotlib 2.2.3 on Python 2.7.15:
I've found that the .set_alpha() function works on many legend objects, but unfortunately, many legend objects have several pieces (such as the output of errorbar()) and the .set_alpha() call will only affect one of them.
One can use .get_legend_handles_labels() and then loop through parts of the handles and .set_alpha(), but unfortunately, copy.deepcopy() does not seem to work on the list of handles, so the plot itself will be affected. The best workaround I could find was to save the original alphas, .set_alpha() to what I wanted, create the legend, then reset the plot alphas back to their original values. It would be much cleaner if I could deepcopy handles (I wouldn't have to save alpha values or reset them), but I could not do this in python2.7 (maybe this depends on what objects are in the legend).
f,ax=plt.subplots(1)
ax.plot( ... )
def legend_alpha(ax,newalpha=1.0):
#sets alpha of legends to some value
#this would be easier if deepcopy worked on handles, but it doesn't
handles,labels=ax.get_legend_handles_labels()
alphass=[None]*len(handles) #make a list to hold lists of saved alpha values
for k,handle in enumerate(handles): #loop through the legend entries
alphas=[None]*len(handle) #make a list to hold the alphas of the pieces of this legend entry
for i,h in enumerate(handle): #loop through the pieces of this legend entry (there could be a line and a marker, for example)
try: #if handle was a simple list of parts, then this will work
alphas[i]=h.get_alpha()
h.set_alpha(newalpha)
except: #if handle was a list of parts which themselves were made up of smaller subcomponents, then we must go one level deeper still.
#this was needed for the output of errorbar() and may not be needed for simpler plot objects
alph=[None]*len(h)
for j,hh in enumerate(h):
alph[j]=hh.get_alpha() #read the alpha values of the sub-components of the piece of this legend entry
hh.set_alpha(newalpha)
alphas[i]=alph #save the list of alpha values for the subcomponents of this piece of this legend entry
alphass[k]=alphas #save the list of alpha values for the pieces of this legend entry
leg=ax.legend(handles,labels) #create the legend while handles has updated alpha values
for k,handle in enumerate(handles): #loop through legend items to restore origina alphas on the plot
for i,h in enumerate(handle): #loop through pieces of this legend item to restore alpha values on the plot
try:
h.set_alpha(alphass[k][i])
except:
for j,hh in enumerate(h): #loop through sub-components of this piece of this legend item to restore alpha values
hh.set_alpha(alphass[k][i][j])
return leg
leg=legend_alpha(ax)
leg.draggable()
In my case, set_alpha(1) also modified the edgecolors, which I didn't want: I had "invisible" edges, and setting alpha to opaque made them visible in the legend. The following snippet (OOP) changes the opacity of the face without changing the border color:
leg = ax.legend()
for lh in leg.legendHandles:
fc_arr = lh.get_fc().copy()
fc_arr[:, -1] = 1 # set opacity here
lh.set_fc(fc_arr)
Note the call to .copy(), if we don't do this it will modify the opacity for the whole plot. Calling copy means we are only modifying the facecolor inside the legend box.
Alternatively, you can add this function to your library:
def opaque_legend(ax):
"""
Calls legend, and sets all the legend colors opacity to 100%.
Returns the legend handle.
"""
leg = ax.legend()
for lh in leg.legendHandles:
fc_arr = lh.get_fc().copy()
fc_arr[:, -1] = 1
lh.set_fc(fc_arr)
return leg
And then simply replace leg = ax.legend() with leg = opaque_legend(ax). Hope this helps!
Andres
Instead of messing up with the opacity of the legend, I found another way. Firstly, I create a plot line with the style I want the legend to be. Then I change the plot line style, and, miraculously, the legend style remains intact. MWE:
plt.plot(x, y, 'ro', label='label')
for lh in plt.gca().get_legend_handles_labels():
lh[0].set_alpha(new_alpha)
I'd like to explain, why it works, but I can't. Neither I'm sure that it works for all backends.
And yes, I know that the question is old. As it still appears in Google, I'll find it later and help my future self.

Categories

Resources