Legend Size and plotting - size

enter image description hereHaving trouble finding a solution to make a legend readable. If I make it bigger the text is still the same scale. I am trying to make the font bold, but having trouble making appended handles bold. Any suggestions?
I tried Changing the fig size and font size, but the legend font runs out of room. Bold would be the solution, but I'm appending handles and they aren't coming out bold, can't figure out where to modify that in my code. Any help appreciated.
fig = plt.figure()
legend_properties = {'weight':'bold'}
fig, ax = plt.subplots(figsize=(8,5))
fit = ax.scatter(x,y,color='none', edgecolor='r',linewidth=1, alpha=0.9,label = 'rockfalls')
Linefit_cutoff = ax.plot(x2,q2,'k',linewidth=1,label='F(V) = $%.2f V^{-%.2f}$' %(c2[0],c2[1]))
Lifefit_all = ax.plot(x,q,'k',linestyle='--',linewidth=1,label='F(V) = $%.2f V^{-%.2f}$' %(c[0],c[1]))
ax.set_xscale("log")
ax.set_yscale("log")
ax.grid(visible=True, which='major', color='gray', linestyle='-',linewidth=0.8, alpha=0.5)
ax.set_xlabel('Rockfall volume (m$^3$)',fontsize=12)
ax.set_ylabel('Cumulative number of events / year', fontsize=12)
plt.title('GPA Rockfalls 1952-2021', fontsize=14,weight='bold')
handles, labels = ax.get_legend_handles_labels()
r_sq = mpatches.Patch(color='w', label='Volumes > %.0f m$^3$, $R^2$=%.3f' %(CutoffV2, R2_2))
samples = mpatches.Patch(color='w', label='Samples = %.0f/%.0f' %(size, size_tot))
[enter image description here](https://i.stack.imgur.com/xQkcX.png)
r_sq2 = mpatches.Patch(color='w', label='All Volumes, $R^2$=%.3f' %(R2))
handles.append(r_sq)
handles.append(r_sq2)
handles.append(samples)
plt.legend(handles=handles,fontsize=16,prop=legend_properties)re

Related

How do we align marker and text in legends vertically in Matplotlib?

When the marker in a legend is a dot, dot and text are not aligned vertically. To solve this I tried following:
l = ax.legend()
for text in l.texts:
text.set_va('center') # Is there some setting for this in matplotlibrc, too??
plt.show()
The vertical alignment of text in a legend seems to be baseline. But no matter whether I choose center, bottom or baseline, etc., things are off:
Zooming in, this is what Matplotlib gives us out of the box:
What I want is also what other software like Inkscape gives me, when aligning two objects vertically:
Can Matplotlib do this for me/us?
This appears to work:
Set it to display only a single scatterpoint per legend entry by setting scatterpoints=1 in the call to legend()
Set the vertical offset of this point to 0 by setting scatteryoffsets=[0] in the call to legend()
After creating the legend, iterate through its text labels and set their vertical alignment to center_baseline, using for t in l.get_texts(): t.set_va('center_baseline')
figure(figsize=(2,2))
scatter([0],[0],marker='s',s=20,label='Thing 1')
scatter([1],[0],marker='s',s=20,label='t')
scatter([2],[0],marker='s',s=20,label='T¹₁')
l = legend(scatterpoints=1,scatteryoffsets=[0],handletextpad=-0.5)
for t in l.get_texts(): t.set_va('center_baseline')
Here is what I do:
import numpy as np
import matplotlib
matplotlib.use('Agg')
matplotlib.rcParams['text.latex.preamble'] = r'\usepackage{amsmath}'
matplotlib.rc('text', usetex = True)
from matplotlib import pyplot as py
## setup figure
figure = py.figure(figsize = (7.5, 5.0))
axs = [py.subplot(1, 1, 1)]
## make plot
xs = np.linspace(0.0, np.pi, 100)
ys = np.sin(xs)
axs[0].plot(xs, ys, color = 'dodgerblue', label = r'$n = 1$')
ys = np.sin(2.0 * xs)
axs[0].plot(xs, ys, color = 'seagreen', label = r'$n = 2$')
axs[0].axhline(0.0, color = 'gray', linestyle = 'dashed')
## vertical alignment
legends = axs[0].legend(frameon = False, fontsize = 25, loc = 'lower left')
shift = np.average([_.get_window_extent(renderer = figure.canvas.get_renderer()).height for _ in legends.get_texts()])
shift /= 3.6
for _ in legends.get_texts():
_.set_va('center') ## va is alias for verticalalignment
_.set_position((0, shift))
## save figure
name = 'test.pdf'
py.tight_layout()
py.savefig(name)
py.close()
It is, however, complicated and requires manual adjustments,
I am still looking for better solutions.

matplotlit colorbar title hangs outside figure

What is the best way to specify my colorbar legend location while ensuring the legend title is within the figure? Sometimes the location will be upper right, as shown here; but in other plots it will be variable, upper/lower left/right.
It is okay if the solution doesn't use inset_axes().
Alternative Solution:
It would also be okay if the colorbar legend is to the right of the subplot, if the "My Legend" title is vertical and on the left, and the tick labels are on the right and horizontal (I don't know how to do that).
Using Python 3.8.
## Second Plot
vals2 = ax2.scatter(df.x, df.y, edgecolors = 'none', c = df.z,
norm = mcolors.LogNorm(), cmap=rainbow)
ax2.set_aspect('equal')
ax2.set_title('Subplot Title', style='italic');
ax2.set_xlabel('x');
ax2.set_ylabel('y');
cbaxes = inset_axes(ax2, width="30%", height="10%", location = 'upper right')
clb = plt.colorbar(vals2, cax=cbaxes, format = '%1.2f', orientation='horizontal');
clb.ax.set_title('My Legend')
I would still prefer to have the colorbar (with tick labels and title) inside the subplot; but I did find a way to do the Alternative Solution:
vals2 = ax2.scatter(df.x, df.y, edgecolors = 'none', c = df.z,
norm = mcolors.LogNorm(), cmap=rainbow)
ax2.set_aspect('equal')
ax2.set_title('Subplot Title', style='italic');
ax2.set_xlabel('x');
ax2.set_ylabel('y');
clb = fig.colorbar(slips2, ax=ax2, format = '%1.2g', location='right', aspect=25)
clb.ax.set_ylabel('My Legend')
clb.ax.yaxis.set_label_position('left')
The color bar is taller than the subplot because ax2 is constrained to be equal xy aspect ratio based on the limits in another subplot (ax1, not shown).

Matplotlib Gridspsec spacing between rows

I have images with three different dimensions (WxH): 4 images with (174x145), 4 images with (145x145) and 4 images with (145x174). I could remove space between columns, but I cannot remove space between rows. Any suggestions?
This is my code:
fig = plt.figure(figsize=(10, 10))
gs = fig.add_gridspec(3, 4, hspace=0, wspace=0)
for r in range(3):
for c in range(4):
ax = fig.add_subplot(gs[r, c])
ax.imshow(slices[r][c].T, origin="lower", cmap="gray")
ax.axis("off")
As suggested in the comments, you need to set the height_ratios for your GridSpec, but that's not enough. You also need to adjust the size of your figure so that the width/height ratio of the figure matches the total width/height ratios of your images. But herein lies another problem, in that the axes will be scaled when plotting the images (because of aspect='equal') and because they do not all have the same width/height ratios.
The solution that I'm proposing is first to calculate what the dimensions of figures would be, once stretched to a common width size, then use that correct information to adjust the figure size and the height_ratios of the GridSpec.
# this is just for visualization purposes
cmaps = iter([ 'flag', 'prism', 'ocean', 'gist_earth', 'terrain', 'gist_stern',
'gnuplot', 'gnuplot2', 'CMRmap', 'cubehelix', 'brg',
'gist_rainbow', 'rainbow', 'jet', 'nipy_spectral', 'gist_ncar'])
sizes = [(174,145), (145,145), (145,174)]
# create random images
p = []
for s in sizes:
p.append([np.random.random(size=s) for _ in range(4)])
p = np.array(p)
max_w = max([w for w,h in sizes])
new_sizes = np.array([(w*max_w/w, h*max_w/w) for w,h in sizes])
print(new_sizes)
total_w = 4*new_sizes[:,0].sum()
total_h = 3*new_sizes[:,1].sum()
eps=10/total_w
fig = plt.figure(figsize=(eps*total_w,eps*total_h))
gs0 = matplotlib.gridspec.GridSpec(3,4, height_ratios=[h for w,h in new_sizes], hspace=0, wspace=0)
for i in range(3):
for j in range(4):
ax = fig.add_subplot(gs0[i,j])
ax.imshow(p[i,j].T, origin="lower", cmap=next(cmaps))
ax.set_axis_off()
Unfortunately, this solution gets you almost to the desired output, but not quite, probably due to some rounding effect. But it close enough that I think you could use aspect='auto' if you can live with pixels that are ever so slightly not square.
(...)
ax.imshow(p[i,j].T, aspect='auto', origin="lower", cmap=next(cmaps))
(...)

Creating figure with exact size and no padding (and legend outside the axes)

I am trying to make some figures for a scientific article, so I want my figures to have a specific size. I also see that Matplotlib by default adds a lot of padding on the border of the figures, which I don't need (since the figures will be on a white background anyway).
To set a specific figure size I simply use plt.figure(figsize = [w, h]), and I add the argument tight_layout = {'pad': 0} to remove the padding. This works perfectly, and even works if I add a title, y/x-labels etc. Example:
fig = plt.figure(
figsize = [3,2],
tight_layout = {'pad': 0}
)
ax = fig.add_subplot(111)
plt.title('title')
ax.set_ylabel('y label')
ax.set_xlabel('x label')
plt.savefig('figure01.pdf')
This creates a pdf file with exact size 3x2 (inches).
The issue I have is that when I for example add a text box outside the axis (typically a legend box), Matplotlib does not make room for the text box like it does when adding titles/axis labels. Typically the text box is cut off, or does not show in the saved figure at all. Example:
plt.close('all')
fig = plt.figure(
figsize = [3,2],
tight_layout = {'pad': 0}
)
ax = fig.add_subplot(111)
plt.title('title')
ax.set_ylabel('y label')
ax.set_xlabel('x label')
t = ax.text(0.7, 1.1, 'my text here', bbox = dict(boxstyle = 'round'))
plt.savefig('figure02.pdf')
A solution I found elsewhere on SO was to add the argument bbox_inches = 'tight' to the savefig command. The text box is now included like I wanted, but the pdf is now the wrong size. It seems like Matplotlib just makes the figure bigger, instead of reducing the size of the axes like it does when adding titles and x/y-labels.
Example:
plt.close('all')
fig = plt.figure(
figsize = [3,2],
tight_layout = {'pad': 0}
)
ax = fig.add_subplot(111)
plt.title('title')
ax.set_ylabel('y label')
ax.set_xlabel('x label')
t = ax.text(0.7, 1.1, 'my text here', bbox = dict(boxstyle = 'round'))
plt.savefig('figure03.pdf', bbox_inches = 'tight')
(This figure is 3.307x2.248)
Is there any solution to this that covers most cases with a legend just outside the axes?
So the requirements are:
Having a fixed, predefined figure size
Adding a text label or legend outside the axes
Axes and text cannot overlap
The axes, together with the title and axis labels, sits tightly agains the figure border.
So tight_layout with pad = 0, solves 1. and 4. but contradicts 2.
One could think on setting pad to a larger value. This would solve 2. However, since it's is symmetric in all directions, it would contradict 4.
Using bbox_inches = 'tight' changes the figure size. Contradicts 1.
So I think there is no generic solution to this problem.
Something I can come up with is the following: It sets the text in figure coordinates and then resizes the axes either in horizontal or in vertical direction such that there is no overlap between the axes and the text.
import matplotlib.pyplot as plt
import matplotlib.transforms
fig = plt.figure(figsize = [3,2])
ax = fig.add_subplot(111)
plt.title('title')
ax.set_ylabel('y label')
ax.set_xlabel('x label')
def text_legend(ax, x0, y0, text, direction = "v", padpoints = 3, margin=1.,**kwargs):
ha = kwargs.pop("ha", "right")
va = kwargs.pop("va", "top")
t = ax.figure.text(x0, y0, text, ha=ha, va=va, **kwargs)
otrans = ax.figure.transFigure
plt.tight_layout(pad=0)
ax.figure.canvas.draw()
plt.tight_layout(pad=0)
offs = t._bbox_patch.get_boxstyle().pad * t.get_size() + margin # adding 1pt
trans = otrans + \
matplotlib.transforms.ScaledTranslation(-offs/72.,-offs/72.,fig.dpi_scale_trans)
t.set_transform(trans)
ax.figure.canvas.draw()
ppar = [0,-padpoints/72.] if direction == "v" else [-padpoints/72.,0]
trans2 = matplotlib.transforms.ScaledTranslation(ppar[0],ppar[1],fig.dpi_scale_trans) + \
ax.figure.transFigure.inverted()
tbox = trans2.transform(t._bbox_patch.get_window_extent())
bbox = ax.get_position()
if direction=="v":
ax.set_position([bbox.x0, bbox.y0,bbox.width, tbox[0][1]-bbox.y0])
else:
ax.set_position([bbox.x0, bbox.y0,tbox[0][0]-bbox.x0, bbox.height])
# case 1: place text label at top right corner of figure (1,1). Adjust axes height.
#text_legend(ax, 1,1, 'my text here', bbox = dict(boxstyle = 'round'), )
# case 2: place text left of axes, (1, y), direction=="v"
text_legend(ax, 1., 0.8, 'my text here', margin=2., direction="h", bbox = dict(boxstyle = 'round') )
plt.savefig(__file__+'.pdf')
plt.show()
case 1 (left) and case 2 (right):
Doin the same with a legend is slightly easier, because we can directly use the bbox_to_anchor argument and don't need to control the fancy box around the legend.
import matplotlib.pyplot as plt
import matplotlib.transforms
fig = plt.figure(figsize = [3.5,2])
ax = fig.add_subplot(111)
ax.set_title('title')
ax.set_ylabel('y label')
ax.set_xlabel('x label')
ax.plot([1,2,3], marker="o", label="quantity 1")
ax.plot([2,1.7,1.2], marker="s", label="quantity 2")
def legend(ax, x0=1,y0=1, direction = "v", padpoints = 3,**kwargs):
otrans = ax.figure.transFigure
t = ax.legend(bbox_to_anchor=(x0,y0), loc=1, bbox_transform=otrans,**kwargs)
plt.tight_layout(pad=0)
ax.figure.canvas.draw()
plt.tight_layout(pad=0)
ppar = [0,-padpoints/72.] if direction == "v" else [-padpoints/72.,0]
trans2=matplotlib.transforms.ScaledTranslation(ppar[0],ppar[1],fig.dpi_scale_trans)+\
ax.figure.transFigure.inverted()
tbox = t.get_window_extent().transformed(trans2 )
bbox = ax.get_position()
if direction=="v":
ax.set_position([bbox.x0, bbox.y0,bbox.width, tbox.y0-bbox.y0])
else:
ax.set_position([bbox.x0, bbox.y0,tbox.x0-bbox.x0, bbox.height])
# case 1: place text label at top right corner of figure (1,1). Adjust axes height.
#legend(ax, borderaxespad=0)
# case 2: place text left of axes, (1, y), direction=="h"
legend(ax,y0=0.8, direction="h", borderaxespad=0.2)
plt.savefig(__file__+'.pdf')
plt.show()
Why 72? The 72 is the number of points per inch (ppi). This is a fixed typographic unit e.g. fontsizes are always given in points (like 12pt). Because matplotlib defines the padding of the text box in units relative to fontsize, which is points, we need to use 72 to transform back to inches (and then to display coordinates). The default dots per inch (dpi) is not touched here, but is accounted for in fig.dpi_scale_trans. If you want to change dpi you need to make sure the figure dpi is set when creating the figure as well as when saving it (use dpi=.. in the call to plt.figure() as well as plt.savefig()).
As of matplotlib==3.1.3, you can use constrained_layout=True to achieve the desired result. This is currently experimental, but see the docs for a very helpful guide (and the section specifically on legends). Note that the legend will steal space from the plot, but this is unavoidable. I've found that as long as the legend does not take up too much space relative to the size of the plot, then the figure gets saved without cropping anything.
import matplotlib.pyplot as plt
fig, ax = plt.subplots(figsize=(3, 2), constrained_layout=True)
ax.set_title('title')
ax.set_ylabel('y label')
ax.set_xlabel('x label')
ax.plot([0,1], [0,1], label='my text here')
ax.legend(loc='center left', bbox_to_anchor=(1.1, 0.5))
fig.savefig('figure03.pdf')

Updating a plot after show

Say I have created a plot and then I show it
fig1 = plt.figure()
ax = fig1.add_subplot(111)
lt.plot(mat0[21:27,1],mat0[21:27,4],marker='s', label = "21")
lt.plot(mat0[21:27,1],mat0[21:27,3],marker='s', label = "23")
plt.plot(mat0[21:27,1],mat0[21:27,2],marker='s', label = "28")
pl.show()
I then realize that I have missed some lines to plot, how can I update the graph without going through all the process of plotting every single line again?