How to have a common y-label between two subplots? [duplicate] - python

I have the following plot:
fig,ax = plt.subplots(5,2,sharex=True,sharey=True,figsize=fig_size)
and now I would like to give this plot common x-axis labels and y-axis labels. With "common", I mean that there should be one big x-axis label below the whole grid of subplots, and one big y-axis label to the right. I can't find anything about this in the documentation for plt.subplots, and my googlings suggest that I need to make a big plt.subplot(111) to start with - but how do I then put my 5*2 subplots into that using plt.subplots?

This looks like what you actually want. It applies the same approach of this answer to your specific case:
import matplotlib.pyplot as plt
fig, ax = plt.subplots(nrows=3, ncols=3, sharex=True, sharey=True, figsize=(6, 6))
fig.text(0.5, 0.04, 'common X', ha='center')
fig.text(0.04, 0.5, 'common Y', va='center', rotation='vertical')

Since I consider it relevant and elegant enough (no need to specify coordinates to place text), I copy (with a slight adaptation) an answer to another related question.
import matplotlib.pyplot as plt
fig, axes = plt.subplots(5, 2, sharex=True, sharey=True, figsize=(6,15))
# add a big axis, hide frame
fig.add_subplot(111, frameon=False)
# hide tick and tick label of the big axis
plt.tick_params(labelcolor='none', which='both', top=False, bottom=False, left=False, right=False)
plt.xlabel("common X")
plt.ylabel("common Y")
This results in the following (with matplotlib version 2.2.0):

New in Matplotlib v3.4 (pip install matplotlib --upgrade)
supxlabel and supylabel
fig.supxlabel('common_x')
fig.supylabel('common_y')
See example:
import matplotlib.pyplot as plt
for tl, cl in zip([True, False, False], [False, False, True]):
fig = plt.figure(constrained_layout=cl, tight_layout=tl)
gs = fig.add_gridspec(2, 3)
ax = dict()
ax['A'] = fig.add_subplot(gs[0, 0:2])
ax['B'] = fig.add_subplot(gs[1, 0:2])
ax['C'] = fig.add_subplot(gs[:, 2])
ax['C'].set_xlabel('Booger')
ax['B'].set_xlabel('Booger')
ax['A'].set_ylabel('Booger Y')
fig.suptitle(f'TEST: tight_layout={tl} constrained_layout={cl}')
fig.supxlabel('XLAgg')
fig.supylabel('YLAgg')
plt.show()
see more

Without sharex=True, sharey=True you get:
With it you should get it nicer:
fig, axes2d = plt.subplots(nrows=3, ncols=3,
sharex=True, sharey=True,
figsize=(6,6))
for i, row in enumerate(axes2d):
for j, cell in enumerate(row):
cell.imshow(np.random.rand(32,32))
plt.tight_layout()
But if you want to add additional labels, you should add them only to the edge plots:
fig, axes2d = plt.subplots(nrows=3, ncols=3,
sharex=True, sharey=True,
figsize=(6,6))
for i, row in enumerate(axes2d):
for j, cell in enumerate(row):
cell.imshow(np.random.rand(32,32))
if i == len(axes2d) - 1:
cell.set_xlabel("noise column: {0:d}".format(j + 1))
if j == 0:
cell.set_ylabel("noise row: {0:d}".format(i + 1))
plt.tight_layout()
Adding label for each plot would spoil it (maybe there is a way to automatically detect repeated labels, but I am not aware of one).

Since the command:
fig,ax = plt.subplots(5,2,sharex=True,sharey=True,figsize=fig_size)
you used returns a tuple consisting of the figure and a list of the axes instances, it is already sufficient to do something like (mind that I've changed fig,axto fig,axes):
fig,axes = plt.subplots(5,2,sharex=True,sharey=True,figsize=fig_size)
for ax in axes:
ax.set_xlabel('Common x-label')
ax.set_ylabel('Common y-label')
If you happen to want to change some details on a specific subplot, you can access it via axes[i] where i iterates over your subplots.
It might also be very helpful to include a
fig.tight_layout()
at the end of the file, before the plt.show(), in order to avoid overlapping labels.

It will look better if you reserve space for the common labels by making invisible labels for the subplot in the bottom left corner. It is also good to pass in the fontsize from rcParams. This way, the common labels will change size with your rc setup, and the axes will also be adjusted to leave space for the common labels.
fig_size = [8, 6]
fig, ax = plt.subplots(5, 2, sharex=True, sharey=True, figsize=fig_size)
# Reserve space for axis labels
ax[-1, 0].set_xlabel('.', color=(0, 0, 0, 0))
ax[-1, 0].set_ylabel('.', color=(0, 0, 0, 0))
# Make common axis labels
fig.text(0.5, 0.04, 'common X', va='center', ha='center', fontsize=rcParams['axes.labelsize'])
fig.text(0.04, 0.5, 'common Y', va='center', ha='center', rotation='vertical', fontsize=rcParams['axes.labelsize'])

Update:
This feature is now part of the proplot matplotlib package that I recently released on pypi. By default, when you make figures, the labels are "shared" between subplots.
Original answer:
I discovered a more robust method:
If you know the bottom and top kwargs that went into a GridSpec initialization, or you otherwise know the edges positions of your axes in Figure coordinates, you can also specify the ylabel position in Figure coordinates with some fancy "transform" magic.
For example:
import matplotlib.pyplot as plt
import matplotlib.transforms as mtransforms
bottom, top = 0.1, 0.9
fig, axs = plt.subplots(nrows=2, ncols=1, bottom=bottom, top=top)
avepos = 0.5 * (bottom + top)
transform = mtransforms.blended_transform_factory(mtransforms.IdentityTransform(), fig.transFigure) # specify x, y transform
axs[0].yaxis.label.set_transform(transform) # changed from default blend (IdentityTransform(), axs[0].transAxes)
axs[0].yaxis.label.set_position((0, avepos))
axs[0].set_ylabel('Hello, world!')
...and you should see that the label still appropriately adjusts left-right to keep from overlapping with labels, just like normal, but will also position itself exactly between the desired subplots.
Notably, if you omit the set_position call, the ylabel will show up exactly halfway up the figure. I'm guessing this is because when the label is finally drawn, matplotlib uses 0.5 for the y-coordinate without checking whether the underlying coordinate transform has changed.

I ran into a similar problem while plotting a grid of graphs. The graphs consisted of two parts (top and bottom). The y-label was supposed to be centered over both parts.
I did not want to use a solution that depends on knowing the position in the outer figure (like fig.text()), so I manipulated the y-position of the set_ylabel() function. It is usually 0.5, the middle of the plot it is added to. As the padding between the parts (hspace) in my code was zero, I could calculate the middle of the two parts relative to the upper part.
import matplotlib.pyplot as plt
import matplotlib.gridspec as gridspec
# Create outer and inner grid
outerGrid = gridspec.GridSpec(2, 3, width_ratios=[1,1,1], height_ratios=[1,1])
somePlot = gridspec.GridSpecFromSubplotSpec(2, 1,
subplot_spec=outerGrid[3], height_ratios=[1,3], hspace = 0)
# Add two partial plots
partA = plt.subplot(somePlot[0])
partB = plt.subplot(somePlot[1])
# No x-ticks for the upper plot
plt.setp(partA.get_xticklabels(), visible=False)
# The center is (height(top)-height(bottom))/(2*height(top))
# Simplified to 0.5 - height(bottom)/(2*height(top))
mid = 0.5-somePlot.get_height_ratios()[1]/(2.*somePlot.get_height_ratios()[0])
# Place the y-label
partA.set_ylabel('shared label', y = mid)
plt.show()
picture
Downsides:
The horizontal distance to the plot is based on the top part, the bottom ticks might extend into the label.
The formula does not take space between the parts into account.
Throws an exception when the height of the top part is 0.
There is probably a general solution that takes padding between figures into account.

Related

Pyplot - add single text to xaxis (like a tick)

Imagine you have a pyplot with a vertical line (matplotlib.axes.Axes.axvline) at a specific location x. Now I would like to have a text like "COG" on the x-axis at x, like as if it was a tick. It can be either on the visible or non-visible axis or both.
However,
ticks already exist (given array)
shared x-axis for subplots, only lowest visible
I though about using normal text (matplotlib.pyplot.text), but
it would be inside the subplot
it would not be in xaxis relation (at least I didn't find a working way so far)
I feel like manually editing the ticks to add a single item is a not so nice workaround..
Thanks ahead!
Here is an example graph of subplots from matplotlib:
import matplotlib.pyplot as plt
import numpy as np
# Some example data to display
x = np.linspace(0, 2 * np.pi, 400)
y = np.sin(x ** 2)
fig, axs = plt.subplots(2, sharex=True)
fig.suptitle('Vertically stacked subplots')
axs[0].plot(x, y)
axs[1].plot(x, -y)
To add the elements you wanted, you can just use axvline and text; Text elements can be outside of the boundaries of the graph (and in fact the tick labels are Text).
#continued from above:
axs[0].xaxis.set_visible(False)
axs[0].axvline(4.5, color='red')
axs[0].text(4.5, -.05, 'COG', color='red', transform=axs[0].get_xaxis_transform(),
ha='center', va='top')
axs[1].axvline(4.5, color='red')
axs[1].text(4.5, -.05, 'COG', color='red', transform=axs[1].get_xaxis_transform(),
ha='center', va='top')
You can instead add another tick and change its color:
#again, continued from the first code block
axs[0].xaxis.set_visible(False)
axs[0].axvline(4.5, color='red')
axs[0].text(4.5, -.05, 'COG', color='red', transform=axs[0].get_xaxis_transform(),
ha='center', va='top')
ticks = [0, 1, 2, 3, 4, 4.5, 5, 6]
labels = [0, 1, 2, 3, 4, "COG", 5, 6]
axs[1].axvline(4.5, color='red')
axs[1].set_xticks(ticks)
axs[1].set_xticklabels(labels)
axs[1].get_xticklabels()[5].set_color('red')
But, if you don't want ticks on the top graph, then it seems like adding Text (as in the first example) is simplest. Additionally, manually setting the ticks in the second example seems more verbose, and there's the issue of selecting the tick you want to change (I index with axs[1].get_xticklabels()[5] here, but with more ticks/nastier numbers you might need something smarter). So I prefer the first approach better than this, but it might be useful in some cases (like if you want your line to occur on an existing tick).
Using TomsĀ“s first example leads to the desired outcome.
Additionally, for the case of an overlapping text on the label, I searched for neighboring tick labels and set their transparency != 1. Thus, the text "cog" is always visible.
import matplotlib.pyplot as plt
import numpy as np
xV = 4.5
dxV = 1/4 # best 1/4 of the spacing between two ticks
# Some example data to display
x = np.linspace(0, 2 * np.pi, 400)
y = np.sin(x ** 2)
fig, axs = plt.subplots(2, sharex=True)
fig.suptitle('Vertically stacked subplots')
axs[0].plot(x, y)
axs[0].xaxis.set_visible(False)
axs[0].axvline(xV, color='red')
axs[0].text(xV, -.05, 'COG', color='red', transform=axs[0].get_xaxis_transform(),
ha='center', va='top')
axs[1].plot(x, -y)
axs[1].axvline(xV, color='red')
axs[1].text(xV, -.05, 'COG', color='red', transform=axs[1].get_xaxis_transform(),
ha='center', va='top')
# Change Transparency if too close
xticks = axs[1].xaxis.get_major_ticks()
values = axs[1].get_xticks()
# values = axs[1].xaxis.get_major_locator()()
pos = np.where(np.logical_and( (xV-dxV) <= values, (xV+dxV) >= values))[0]
if pos.size > 0:
dist = np.abs(values[pos]-xV)
pos = pos[dist.argmin()]
xticks[pos].label1.set_alpha(0.5)
plt.show()

Using up space left by a missing label when using subplots

I apologise for the titlegore, but I could not figure out how to phrase it in a different way. The problem is best illustrated by the picture below. As you can see, I made figure consisting of 5 subplots using matplotlibs gridspec, which are fit into 4 square panels. The three empty panels have their own sets of x coordinates, and require their own label. However, the data from the first two panels shares the X axis, and (given that the actual label will be lengthy) I'd rather have only a single label and a single set of ticks for both, as shown here.
But as you can see, this leaves a rather large gap of whitespace between the two panels where the label would have gone. And this is what I'd like to solve; I'd like to stretch the two panels in equal amounts to fill up this white space. At the same time the top of the top panel and the bottom of the bottom panel should still align with the subplot to the right, and the bottom of the two panels shouldn't interfere with the position of the row that comes below either. I looked into the documentation on adjusting the panels in the documentation but I couldn't figure it out.
As an aside I'd also like to have a single y-axis label for the two panels, but I think I can fudge that in with fig.text().
The code that generates the above plot:
import numpy as np
from matplotlib import pyplot as plt
from matplotlib import gridspec
xs = np.linspace(0,8*np.pi,101)
ys = np.cos(xs)
fig = plt.figure(figsize=(7.2,4.45*1.5))
gs1 = gridspec.GridSpec(4, 2, figure=fig)
#gs1.update(hspace=0.1)
ax1 = plt.subplot(gs1[0, 0])
ax1.plot(xs, ys)
#ax1.set_xlabel('X')
ax1.set_ylabel('Y1')
ax1.set_xticks([])
ax2 = plt.subplot(gs1[1, 0])
ax2.plot(xs, 0.5*ys)
ax2.set_xlabel('X')
ax2.set_ylabel('Y2')
ax2.set_ylim(-1,1)
gs2 = gridspec.GridSpec(4, 2)
ax3 = plt.subplot(gs2[0:2, 1])
ax3.set_xlabel('X3')
ax3.set_ylabel('Y3')
ax4 = plt.subplot(gs2[2:, 0])
ax4.set_xlabel('X4')
ax4.set_ylabel('Y4')
ax5 = plt.subplot(gs2[2:, 1])
ax5.set_xlabel('X5')
ax5.set_ylabel('Y5')
plt.tight_layout()
You can use a SubplotSpec in one of the quadrants of a 2x2 gridspec.
An example is found int gridspec-using-subplotspec.
Here it would look like
import numpy as np
from matplotlib import pyplot as plt
xs = np.linspace(0,8*np.pi,101)
ys = np.cos(xs)
fig = plt.figure(figsize=(7.2,4.45*1.5))
# 2x2 "outer" GridSpec
gs = fig.add_gridspec(2, 2)
# 2x1 "inner" GridSpec to be used
# in one cell of the outer grid
gs00 = gs[0,0].subgridspec(2, 1)
ax1 = fig.add_subplot(gs00[0])
ax1.plot(xs, ys)
ax1.set_ylabel('Y1')
ax1.set_xticks([])
ax2 = fig.add_subplot(gs00[1])
ax2.plot(xs, 0.5*ys)
ax2.set_xlabel('X')
ax2.set_ylabel('Y2')
ax2.set_ylim(-1,1)
ax3 = fig.add_subplot(gs[0,1])
ax3.set_xlabel('X3')
ax3.set_ylabel('Y3')
ax4 = fig.add_subplot(gs[1,0])
ax4.set_xlabel('X4')
ax4.set_ylabel('Y4')
ax5 = fig.add_subplot(gs[1,1])
ax5.set_xlabel('X5')
ax5.set_ylabel('Y5')
fig.tight_layout()
plt.show()

Prevent grid lines from twin axis to be drawn on top of artists from original axis

I have an axis on which I plot some data and I have another twin axis which I use to draw grid lines at specific tick positions (other than the ticks of the original axis):
import matplotlib.pyplot as plt
import numpy as np
f, ax = plt.subplots()
ax.set_xlim([0, 1])
ax2 = ax.twiny()
ax2.set_xlim([0, 1])
ax2.set_xticks(np.linspace(0, 1, 11))
ax2.xaxis.grid()
x = np.linspace(0, 1, 100)
ax.plot(x, np.sin(x), label='sin(x)')
ax.legend()
plt.show()
Now this has the undesirable effect that the grid lines of the twin axes are drawn on top of the legend and line plot of the original axis. As far as I understand this is because matplotlib draws the axes in the order they were created and for that reason zorder won't help (because zorder only specifies the order among the artists of a single axis).
I know I could plot the data on the twin axis ax2 instead (followed by ax2.legend()) but I'd prefer to have the setup as is. Instead changing the order in which the two axes are drawn should solve the problem, but I couldn't figure out how to do that. There is f.get_axes() which seems to return the axes in the order they were created but no option to revert it.
Or maybe there exists even another solution?
You can change the zorder of the axes themselves.
ax.set_zorder(2)
ax2.set_zorder(1)
ax.patch.set_visible(False)

In matplotlib, how do you display an axis on both sides of the figure?

I want to draw a plot with matplotlib with axis on both sides of the plot, similar to this plot (the color is irrelevant to this question):
How can I do this with matplotlib?
Note: contrary to what is shown in the example graph, I want the two axis to be exactly the same, and want to show only one graph. Adding the two axis is only to make reading the graph easier.
You can use tick_params() (this I did in Jupyter notebook):
import matplotlib.pyplot as plt
bar(range(10), range(10))
tick_params(labeltop=True, labelright=True)
Generates this image:
UPD: added a simple example for subplots. You should use tick_params() with axis object.
This code sets to display only top labels for the top subplot and bottom labels for the bottom subplot (with corresponding ticks):
import matplotlib.pyplot as plt
f, axarr = plt.subplots(2)
axarr[0].bar(range(10), range(10))
axarr[0].tick_params(labelbottom=False, labeltop=True, labelleft=False, labelright=False,
bottom=False, top=True, left=False, right=False)
axarr[1].bar(range(10), range(10, 0, -1))
axarr[1].tick_params(labelbottom=True, labeltop=False, labelleft=False, labelright=False,
bottom=True, top=False, left=False, right=False)
Looks like this:
There are a couple of relevant examples in the online documentation:
Two Scales (seems to do exactly what you're asking for)
Dual Fahrenheit and Celsius
I've done this previously using the following:
# Create figure and initial axis
fig, ax0 = plt.subplots()
# Create a duplicate of the original xaxis, giving you an additional axis object
ax1 = ax.twinx()
# Set the limits of the new axis from the original axis limits
ax1.set_ylim(ax0.get_ylim())
This will exactly duplicate the original y-axis.
Eg:
ax = plt.gca()
plt.bar(range(3), range(1, 4))
plt.axhline(1.75, color="gray", ls=":")
twin_ax = ax.twinx()
twin_ax.set_yticks([1.75])
twin_ax.set_ylim(ax.get_ylim())

Get legend as a separate picture in Matplotlib

I'm developing a Web application and want to display a figure and its legend in different locations on the page. Which means I need to save the legend as a separate png file. Is this possible in Matplotlib in a more or less straightforward way?
This could work:
import pylab
fig = pylab.figure()
figlegend = pylab.figure(figsize=(3,2))
ax = fig.add_subplot(111)
lines = ax.plot(range(10), pylab.randn(10), range(10), pylab.randn(10))
figlegend.legend(lines, ('one', 'two'), 'center')
fig.show()
figlegend.show()
figlegend.savefig('legend.png')
You may limit the saved region of a figure to the bounding box of the legend using the bbox_inches argument to fig.savefig. Below to versions of a function which you can simply call with the legend you want to save as argument. You may either use the legend created in the original figure here (and remove it afterwards, legend.remove()) or you may create a new figure for the legend and simply use the function as it is.
Export legend boundingbox
In case the complete legend shall be saved, the bounding box supplied to the bbox_inches argument would be simply the transformed bounding box of the legend. This works well if the legend has no border around it.
import matplotlib.pyplot as plt
colors = ["crimson", "purple", "gold"]
f = lambda m,c: plt.plot([],[],marker=m, color=c, ls="none")[0]
handles = [f("s", colors[i]) for i in range(3)]
labels = colors
legend = plt.legend(handles, labels, loc=3, framealpha=1, frameon=False)
def export_legend(legend, filename="legend.png"):
fig = legend.figure
fig.canvas.draw()
bbox = legend.get_window_extent().transformed(fig.dpi_scale_trans.inverted())
fig.savefig(filename, dpi="figure", bbox_inches=bbox)
export_legend(legend)
plt.show()
Export extended legend bounding box
If there is a border around the legend, the above solution may be suboptimal. In this case it makes sense to extend the bounding box by some pixels to include the border to its full.
import numpy as np
import matplotlib.pyplot as plt
colors = ["crimson", "purple", "gold"]
f = lambda m,c: plt.plot([],[],marker=m, color=c, ls="none")[0]
handles = [f("s", colors[i]) for i in range(3)]
labels = colors
legend = plt.legend(handles, labels, loc=3, framealpha=1, frameon=True)
def export_legend(legend, filename="legend.png", expand=[-5,-5,5,5]):
fig = legend.figure
fig.canvas.draw()
bbox = legend.get_window_extent()
bbox = bbox.from_extents(*(bbox.extents + np.array(expand)))
bbox = bbox.transformed(fig.dpi_scale_trans.inverted())
fig.savefig(filename, dpi="figure", bbox_inches=bbox)
export_legend(legend)
plt.show()
use pylab.figlegend(..) and get_legend_handles_labels(..):
import pylab, numpy
x = numpy.arange(10)
# create a figure for the data
figData = pylab.figure()
ax = pylab.gca()
for i in xrange(3):
pylab.plot(x, x * (i+1), label='line %d' % i)
# create a second figure for the legend
figLegend = pylab.figure(figsize = (1.5,1.3))
# produce a legend for the objects in the other figure
pylab.figlegend(*ax.get_legend_handles_labels(), loc = 'upper left')
# save the two figures to files
figData.savefig("plot.png")
figLegend.savefig("legend.png")
It can be tricky though to get the size of the legend figure right in an automated manner.
It is possible to use axes.get_legend_handles_labels to get the legend handles and labels from one axes object and to use them to add them to an axes in a different figure.
# create a figure with one subplot
fig = plt.figure()
ax = fig.add_subplot(111)
ax.plot([1,2,3,4,5], [1,2,3,4,5], 'r', label='test')
# save it *without* adding a legend
fig.savefig('image.png')
# then create a new image
# adjust the figure size as necessary
figsize = (3, 3)
fig_leg = plt.figure(figsize=figsize)
ax_leg = fig_leg.add_subplot(111)
# add the legend from the previous axes
ax_leg.legend(*ax.get_legend_handles_labels(), loc='center')
# hide the axes frame and the x/y labels
ax_leg.axis('off')
fig_leg.savefig('legend.png')
If for some reason you want to hide only the axes label, you can use:
ax.xaxis.set_visible(False)
ax.yaxis.set_visible(False)
or if, for some weirder reason, you want to hide the axes frame but not the axes labels you can use:
ax.set_frame_on(False)
ps: this answer has been adapted from my answer to a duplicate question
I've found that the easiest way is just to create your legend and then just turn off the axis with plt.gca().set_axis_off():
# Create a color palette
palette = dict(zip(['one', 'two'], ['b', 'g']))
# Create legend handles manually
handles = [mpl.patches.Patch(color=palette[x], label=x) for x in palette.keys()]
# Create legend
plt.legend(handles=handles)
# Get current axes object and turn off axis
plt.gca().set_axis_off()
plt.show()
This calculates the size of the legend automatically. If mode == 1, the code is similar to Steve Tjoa's answer, while mode == 2 is similar Andre Holzner's answer.
The loc parameter must be set to 'center' to make it work (but I do not know why this is necessary).
mode = 1
#mode = 2
import pylab
fig = pylab.figure()
if mode == 1:
lines = fig.gca().plot(range(10), pylab.randn(10), range(10), pylab.randn(10))
legend_fig = pylab.figure(figsize=(3,2))
legend = legend_fig.legend(lines, ('one', 'two'), 'center')
if mode == 2:
fig.gca().plot(range(10), pylab.randn(10), range(10), pylab.randn(10), label='asd')
legend_fig = pylab.figure()
legend = pylab.figlegend(*fig.gca().get_legend_handles_labels(), loc = 'center')
legend.get_frame().set_color('0.70')
legend_fig.canvas.draw()
legend_fig.savefig('legend_cropped.png',
bbox_inches=legend.get_window_extent().transformed(legend_fig.dpi_scale_trans.inverted()))
legend_fig.savefig('legend_original.png')
Original (uncropped) legend:
Cropped legend:
Inspired by Maxim and ImportanceOfBeingErnest's answers,
def export_legend(ax, filename="legend.pdf"):
fig2 = plt.figure()
ax2 = fig2.add_subplot()
ax2.axis('off')
legend = ax2.legend(*ax.get_legend_handles_labels(), frameon=False, loc='lower center', ncol=10,)
fig = legend.figure
fig.canvas.draw()
bbox = legend.get_window_extent().transformed(fig.dpi_scale_trans.inverted())
fig.savefig(filename, dpi="figure", bbox_inches=bbox)
which allows me to save legend horizontally in a separate file. As an example
In November 2020, I tried almost everything on this post, but none worked for me. After struggling for a while, I found a solution that does what I want.
Pretend you want to draw a figure and a legend separately that looks like below (apparently I don't have enough reputation to embed pictures in a post; click the links to see the picture).
import matplotlib.pyplot as plt
%matplotlib inline
fig, ax = plt.subplots()
ax.plot([1, 2, 3], label="test1")
ax.plot([3, 2, 1], label="test2")
ax.legend()
target figure
You can separate the figure and the legend in two different ax objects:
fig, [ax1, ax2] = plt.subplots(1, 2)
ax1.plot([1, 2, 3], label="test1")
ax1.plot([3, 2, 1], label="test2")
ax2.plot([1, 2, 3], label="test1")
ax2.plot([3, 2, 1], label="test2")
h, l = ax2.get_legend_handles_labels()
ax2.clear()
ax2.legend(h, l, loc='upper left')
ax2.axis('off')
fixed figure 1
You can easily control where the legend should go:
fig, [ax1, ax2] = plt.subplots(2, 1)
ax1.plot([1, 2, 3], label="test1")
ax1.plot([3, 2, 1], label="test2")
ax2.plot([1, 2, 3], label="test1")
ax2.plot([3, 2, 1], label="test2")
h, l = ax2.get_legend_handles_labels()
ax2.clear()
ax2.legend(h, l, loc='upper left')
ax2.axis('off')
fixed figure 2
I'd like to add a small contribution for the specific case where your legend is customized such as here: https://matplotlib.org/3.1.1/gallery/text_labels_and_annotations/custom_legends.html
In that case, you might have to go for a different method. I've been exposed to that problem and the answers above did not work for me.
The code below sets-up the legend.
import cmocean
import matplotlib
from matplotlib.lines import Line2D
lightcmap = cmocean.tools.lighten(cmo.solar, 0.7)
custom_legend = []
custom_legend_strings=['no impact - high confidence', 'no impact - low confidence', 'impact - low confidence', 'impact - high confidence']
for nbre_classes in range(len(custom_legend_strings)):
custom_legend.append(Line2D([0], [0], color=lightcmap(nbre_classes/len(custom_legend_strings)), lw=4))
I think because this kind of legend is attached the axes, a little trick was necessary :
center the legend with a big font to make it take most of the available space and do not erase but set the axes to off :
fig,ax = plt.subplots(figsize=(10,10))
ax.legend(custom_legend,custom_legend_strings, loc = 10, fontsize=30)
plt.axis('off')
fig.savefig('legend.png', bbox_inches='tight')
The result is :
the result
I was not able to find exactly what I wanted in the existing answer so I implemented it. It wanted to generate a standalone legend without any figure attached nor visual "glitches". I came up with this:
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.patches import Patch
palette = dict(zip(['one', 'two', 'tree', 'four'], ['b', 'g', 'r', 'k']))
def export_legend(palette, dpi="figure", filename="legend.png"):
# Create empty figure with the legend
handles = [Patch(color=c, label=l) for l, c in palette.items()]
fig = plt.figure()
legend = fig.gca().legend(handles=handles, framealpha=1, frameon=True)
# Render the legend
fig.canvas.draw()
# Export the figure, limiting the bounding box to the legend area,
# slighly extended to ensure the surrounding rounded corner box of
# is not cropped. Transparency is enabled, so it is not an issue.
bbox = legend.get_window_extent().padded(2)
bbox = bbox.transformed(fig.dpi_scale_trans.inverted())
fig.savefig(filename, dpi=dpi, transparent=True, bbox_inches=bbox)
# Delete the legend along with its temporary figure
plt.close(fig)
export_legend(palette, dpi=400)
Note that the surrounding background is transparent, so adding the legend on top of a figure should not have white "glitches" in the corners, nor issue of cropped border.
And if you don't want to save the image of disk, here is the trick !
DPI = 400
def export_legend(palette):
# Create empty figure with the legend
handles = [Patch(color=c, label=l) for l, c in palette.items()]
fig = plt.figure()
legend = fig.gca().legend(handles=handles, framealpha=1, frameon=True)
# Render the legend
fig.canvas.draw()
# Export the figure, limiting the bounding box to the legend area,
# slighly extended to ensure the surrounding rounded corner box of
# is not cropped. Transparency is enabled, so it is not an issue.
bbox = legend.get_window_extent().padded(2)
bbox_inches = bbox.transformed(fig.dpi_scale_trans.inverted())
bbox_inches = bbox.from_extents(np.round(bbox_inches.extents * 400) / 400)
io_buf = io.BytesIO()
fig.savefig(io_buf, format='rgba', dpi=DPI, transparent=True, bbox_inches=bbox_inches)
io_buf.seek(0)
img_raw = io_buf.getvalue()
img_size = (np.asarray(bbox_inches.bounds)[2:] * DPI).astype(int)
# Delete the legend along with its temporary figure
plt.close(fig)
return img_raw, img_size
The raw buffer can be read directly using PIL or whatever dealing with raw buffer.
So I was playing with this idea and simplest thing I have found is this (works with multiple axes):
def export_legend(filename="legend.png", fig=fig):
legend = fig.legend(framealpha=1)
fig2 = legend.figure
fig2.canvas.draw()
bbox = legend.get_window_extent().transformed(fig2.dpi_scale_trans.inverted())
fig2.savefig(filename, dpi="figure", bbox_inches=bbox, facecolor="w")
legend.remove() # removes legend from showing on plot
export_legend()
Output of a function (I hided labels with boxes):
fig is from fig, ax = plt.subplots()
If you want legend to still show on plot you can use (for example):
fig.legend(loc="upper right", bbox_to_anchor=(1, 1), bbox_transform=ax.transAxes)

Categories