How do I extend the margin at the bottom of a figure in Matplotlib? - matplotlib

The following screenshot shows my x-axis.
I added some labels and rotated them by 90 degrees in order to better read them. However, pyplot truncates the bottom such that I'm not able to completely read the labels.
How do I extend the bottom margin in order to see the complete labels?

Two retroactive ways:
fig, ax = plt.subplots()
# ...
fig.tight_layout()
Or
fig.subplots_adjust(bottom=0.2) # or whatever
Here's a subplots_adjust example: http://matplotlib.org/examples/pylab_examples/subplots_adjust.html
(but I prefer tight_layout)

A quick one-line solution that has worked for me is to use pyplot's auto tight_layout method directly, available in Matplotlib v1.1 onwards:
plt.tight_layout()
This can be invoked immediately before you show the plot (plt.show()), but after your manipulations on the axes (e.g. ticklabel rotations, etc).
This convenience method avoids manipulating individual figures of subplots.
Where plt is the standard pyplot from:
import matplotlib.pyplot as plt

fig.savefig('name.png', bbox_inches='tight')
works best for me, since it doesn't reduce the plot size compared to
fig.tight_layout()

Subplot-adjust did not work for me, since the whole figure would just resize with the labels still out of bounds.
A workaround I found was to keep the y-axis always a certain margin over the highest or minimum y-values:
x1,x2,y1,y2 = plt.axis()
plt.axis((x1,x2,y1 - 100 ,y2 + 100))

fig, ax = plt.subplots(tight_layout=True)

This is rather complicated, but it gives a general and neat solution.
import numpy as np
value1 = 3
xvalues = [0, 1, 2, 3, 4]
line1 = [2.0, 3.0, 2.0, 5.0, 4.0]
stdev1 = [0.1, 0.2, 0.1, 0.4, 0.3]
line2 = [1.7, 3.1, 2.5, 4.8, 4.2]
stdev2 = [0.12, 0.18, 0.12, 0.3, 0.35]
max_times = [max(line1+stdev1),max(line2+stdev2)]
min_times = [min(line1+stdev1),min(line2+stdev2)]
font_size = 25
max_total = max(max_times)
min_total = min(min_times)
max_minus_min = max_total - min_total
step_size = max_minus_min/10
head_space = (step_size*3)
plt.figure(figsize=(15, 15))
plt.errorbar(xvalues, line1, yerr=stdev1, fmt='', color='b')
plt.errorbar(xvalues, line2, yerr=stdev2, fmt='', color='r')
plt.xlabel("xvalues", fontsize=font_size)
plt.ylabel("lines 1 and 2 Test "+str(value1), fontsize=font_size)
plt.title("Let's leave space for the legend Experiment"+ str(value1), fontsize=font_size)
plt.legend(("Line1", "Line2"), loc="upper left", fontsize=font_size)
plt.tick_params(labelsize=font_size)
plt.yticks(np.arange(min_total, max_total+head_space, step=step_size) )
plt.grid()
plt.tight_layout()
Result:

Related

Seaborn jointplot link x-axis to Matplotlib subplots

Is there a way to add additional subplots created with vanilla Matplotlib to (below) a Seaborn jointplot, sharing the x-axis? Ideally I'd like to control the ratio between the jointplot and the additional plots (similar to gridspec_kw={'height_ratios':[3, 1, 1]}
I tried to fake it by tuning figsize in the Matplotlib subplots, but obviously it doesn't work well when the KDE curves in the marginal plot change. While I could manually resize the output PNG to shrink/grow one of the figures, I'd like to have everything aligned automatically.
I know this is tricky with the way the joint grid is set up, but maybe it is reasonably simple for someone fluent in the underpinnings of Seaborn.
Here is a minimal working example, but there are two separate figures:
import matplotlib.pyplot as plt
import seaborn as sns
%matplotlib inline
Figure 1
diamonds = sns.load_dataset('diamonds')
g = sns.jointplot(
data=diamonds,
x="carat",
y="price",
hue="cut",
xlim=(1, 2),
)
g.ax_marg_x.remove()
Figure 2
fig, (ax1, ax2) = plt.subplots(2,1,sharex=True)
ax1.scatter(x=diamonds["carat"], y=diamonds["depth"], color="gray", edgecolor="black")
ax1.set_xlim([1, 2])
ax1.set_ylabel("depth")
ax2.scatter(x=diamonds["carat"], y=diamonds["table"], color="gray", edgecolor="black")
ax2.set_xlabel("carat")
ax2.set_ylabel("table")
Desired output:
I think this is a case where setting up the figure using matplotlib functions is going to be better than working backwards from a seaborn figure layout that doesn't really match the use-case.
If you have a non-full subplot grid, you'll have to decide whether you want to (A) set up all the subplots and then remove the ones you don't want or (B) explicitly add each of the subplots you do want. Let's go with option A here.
figsize = (6, 8)
gridspec_kw = dict(
nrows=3, ncols=2,
width_ratios=[5, 1],
height_ratios=[4, 1, 1],
)
subplot_kw = dict(sharex="col", sharey="row")
fig = plt.figure(figsize=figsize, constrained_layout=True)
axs = fig.add_gridspec(**gridspec_kw).subplots(**subplot_kw)
sns.kdeplot(data=df, y="price", hue="cut", legend=False, ax=axs[0, 1])
sns.scatterplot(data=df, x="carat", y="price", hue="cut", ax=axs[0, 0])
sns.scatterplot(data=df, x="carat", y="depth", color=".2", ax=axs[1, 0])
sns.scatterplot(data=df, x="carat", y="table", color=".2", ax=axs[2, 0])
axs[0, 0].set(xlim=(1, 2))
axs[1, 1].remove()
axs[2, 1].remove()
BTW, this is almost a bit easier with plt.subplot_mosaic, but it does not yet support axis sharing.
You could take the figure created by jointplot(), move its padding (with subplots_adjust()) and add 2 extra axes.
The example code will need some tweaking for each particular situation.
import matplotlib.pyplot as plt
from mpl_toolkits.axes_grid1.inset_locator import inset_axes
import seaborn as sns
diamonds = sns.load_dataset('diamonds')
g = sns.jointplot(data=diamonds, x="carat", y="price", hue="cut",
xlim=(1, 2), height=12)
g.ax_marg_x.remove()
g.fig.subplots_adjust(left=0.08, right=0.97, top=1.05, bottom=0.45)
axins1 = inset_axes(g.ax_joint, width="100%", height="30%",
bbox_to_anchor=(0, -0.4, 1, 1),
bbox_transform=g.ax_joint.transAxes, loc=3, borderpad=0)
axins2 = inset_axes(g.ax_joint, width="100%", height="30%",
bbox_to_anchor=(0, -0.75, 1, 1),
bbox_transform=g.ax_joint.transAxes, loc=3, borderpad=0)
shared_x_group = g.ax_joint.get_shared_x_axes()
shared_x_group.remove(g.ax_marg_x)
shared_x_group.join(g.ax_joint, axins1)
shared_x_group.join(g.ax_joint, axins2)
axins1.scatter(x=diamonds["carat"], y=diamonds["depth"], color="grey", edgecolor="black")
axins1.set_ylabel("depth")
axins2.scatter(x=diamonds["carat"], y=diamonds["table"], color="grey", edgecolor="black")
axins2.set_xlabel("carat")
axins2.set_ylabel("table")
g.ax_joint.set_xlim(1, 2)
plt.setp(axins1.get_xticklabels(), visible=False)
plt.show()
PS: How to share x axes of two subplots after they have been created contains some info about sharing axes (although here you simply get the same effect by setting the xlims for each of the subplots).
The code to position the new axes has been adapted from this tutorial example.

Why setting fixed colorbar failed in this case?

I am trying to make a bunch of polar view plots using the same colorbar. However, the colorbars differ after setting the plotting limits. In the code snippet below, I randomly created 5 maps but plotted in a fixed range, but the output figures are still different in colorbar.
from numpy import linspace, pi, ndarray, random
import matplotlib
matplotlib.use('Agg')
from matplotlib.pyplot import figure
lon = linspace(start=0, stop=2*pi, num=100)
colat = linspace(start=0, stop=9, num=10)
emission = ndarray(shape=(10, 100, 5), dtype=float)
for t in range(5):
emission[:, :, t] = random.rand(10, 100)
fig = figure(num='emission', figsize=(15, 15))
em_pos = [0.05, 0.1, 0.8, 0.8]
emc_pos = [0.9, 0.1, 0.05, 0.8]
for t in range(5):
fig.clear()
ax = fig.add_subplot(121, polar=True, position=em_pos)
axcont = ax.contourf(lon, colat, emission[:, :, t], vmin=0, vmax=2)
axc = fig.add_subplot(122, position=emc_pos)
fig.colorbar(mappable=axcont, cax=axc)
fig.savefig(fname='emission{0:d}.png'.format(t), format='png')
The problem seems to be solved. It is not a problem of colorbar, but a problem of contourf. When I replaced
ax.contourf(lon, colat, emission[:, :, t], vmin=0, vmax=2)
with
ax.pcolormesh(lon, colat, emission[:, :, t], vmin=0, vmax=2)
Then the colorbar shows the proper range. Indeed it is not a full solution, pcolormesh differs in some aspects from contourf, but it meets my needs.

how to remove the white space of invisiable axes in matplotlib during active plot?

I want to completely remove white space around my axes during active plot (not save_fig as others asked).
Here we cannot use bbox_inches='tight'. I can use tight_layout(pad=0).
When axis is on, it works fine, it shows all the ticks and x-y labels.
However, in some cases, I set the axis off. What I expected is to see the contents expand to fill up the empty space where the axes are. However, this does not work. It still keep the padding as there are still x-y labels and axes.
How can I remove the white space of invisible axes objects?
edit:
I am aware that I can use ax.set_yticks([]) and ax.set_xticks([]) to turn those off. But this is clumsy, I have to remember the the ticks before I clear them. And if I remove-then-add those ticks. The ticks cannot automatically update any more.
I wonder is there any more straightforward way to do this?
We can still see there is a small border spacing even after removing all ticks. If someone can come up a way to remove that too. It will be fantastic.
I would also like to keep the title if there is one. Thus the hard-coded ax.set_position([0,0,1,x]) is not very good for this usage. Surely we can still try to get the top spacing when there is a title, but if someone can provide a more direct/simple way to handle this, it will be preferred.
Example code:
def demo_tight_layout(w=10, h=6, axisoff=False, removeticks=False):
fig,ax = plt.subplots()
fig.set_facecolor((0.8, 0.8, 0.8))
rect = patches.Rectangle((-w/2, -h/2), w, h, color='#00ffff', alpha=0.5)
ax.add_patch(rect)
ax.plot([-w/2,w/2], [-h/2,h/2])
ax.plot([-w/2,w/2], [h/2,-h/2])
ax.set_ylabel("ylabel")
ax.margins(0)
_texts = []
if axisoff:
ax.set_axis_off()
_texts.append("axisoff")
if removeticks:
ax.set_xticks([])
ax.set_yticks([])
ax.set_ylabel("")
_texts.append("removeticks")
fig.text(0.5, 0.6, " ".join(_texts))
fig.tight_layout(pad=0)
plt.show()
return fig, ax, text
You may adjust the subplot parameters depending on whether you turned the axis off or not.
import matplotlib.pyplot as plt
from matplotlib import patches
def demo_tight_layout(w=10, h=6, axisoff=False):
fig,ax = plt.subplots()
fig.set_facecolor((0.8, 0.8, 0.8))
rect = patches.Rectangle((-w/2, -h/2), w, h, color='#00ffff', alpha=0.5)
ax.add_patch(rect)
ax.plot([-w/2,w/2], [-h/2,h/2])
ax.plot([-w/2,w/2], [h/2,-h/2])
ax.set_ylabel("ylabel")
ax.margins(0)
_texts = []
fig.tight_layout()
if axisoff:
ax.set_axis_off()
_texts.append("axisoff")
params = dict(bottom=0, left=0, right=1)
if ax.get_title() == "":
params.update(top=1)
fig.subplots_adjust(**params)
fig.text(0.5, 0.6, " ".join(_texts))
plt.show()
Now demo_tight_layout(axisoff=True) produces
and demo_tight_layout(axisoff=False) produces
You need to set the axes position to fill the figure. If you create your figure and plot with
import matplotlib.pyplot as plt
fig = plt.figure()
ax = fig.gca()
ax.plot(some_x_data, some_y_data)
you need to add the following line to fill the figure with the axes:
ax.set_position([0, 0, 1, 1], which='both')
This sets the axes location relative to the figure size in the following way:
[left, bottom, width, height]
So to completely fill the figure use [0, 0, 1, 1] as shown above.
So taking your code, it should look like this (using fill_figure bool to check):
def demo_tight_layout(w=10, h=6, axisoff=False, removeticks=False, fill_figure=False):
fig,ax = plt.subplots()
fig.set_facecolor((0.8, 0.8, 0.8))
rect = patches.Rectangle((-w/2, -h/2), w, h, color='#00ffff', alpha=0.5)
ax.add_patch(rect)
ax.plot([-w/2,w/2], [-h/2,h/2])
ax.plot([-w/2,w/2], [h/2,-h/2])
ax.set_ylabel("ylabel")
ax.margins(0)
_texts = []
if axisoff:
ax.set_axis_off()
_texts.append("axisoff")
if removeticks:
ax.set_xticks([])
ax.set_yticks([])
ax.set_ylabel("")
_texts.append("removeticks")
fig.text(0.5, 0.6, " ".join(_texts))
fig.tight_layout(pad=0)
if fill_figure:
ax.set_position([0, 0, 1, 1], which='both')
plt.show()
return fig, ax, text
ax.set_position needs to be after fig.tight_layout.
If a figure title is needed, there is no direct way to do it. This unluckily can't be avoided. You need to adapt the height parameters manually so that the title fits in the figure, for example with:
ax.set_position([0, 0, 1, .9], which='both')

Labels on Gridspec [duplicate]

I'm facing a problem in showing the legend in the correct format using matplotlib.
EDIT: I have 4 subplots in a figure in 2 by 2 format and I want legend only on the first subplot which has two lines plotted on it. The legend that I got using the code attached below contained endless entries and extended vertically throughout the figure. When I use the same code using linspace to generate fake data the legend works absolutely fine.
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.ticker as mtick
import os
#------------------set default directory, import data and create column output vectors---------------------------#
path="C:/Users/Pacman/Data files"
os.chdir(path)
data =np.genfromtxt('vrp.txt')
x=np.array([data[:,][:,0]])
y1=np.array([data[:,][:,6]])
y2=np.array([data[:,][:,7]])
y3=np.array([data[:,][:,9]])
y4=np.array([data[:,][:,11]])
y5=np.array([data[:,][:,10]])
nrows=2
ncols=2
tick_l=6 #length of ticks
fs_axis=16 #font size of axis labels
plt.rcParams['axes.linewidth'] = 2 #Sets global line width of all the axis
plt.rcParams['xtick.labelsize']=14 #Sets global font size for x-axis labels
plt.rcParams['ytick.labelsize']=14 #Sets global font size for y-axis labels
plt.subplot(nrows, ncols, 1)
ax=plt.subplot(nrows, ncols, 1)
l1=plt.plot(x, y2, 'yo',label='Flow rate-fan')
l2=plt.plot(x,y3,'ro',label='Flow rate-discharge')
plt.title('(a)')
plt.ylabel('Flow rate ($m^3 s^{-1}$)',fontsize=fs_axis)
plt.xlabel('Rupture Position (ft)',fontsize=fs_axis)
# This part is not working
plt.legend(loc='upper right', fontsize='x-large')
#Same code for rest of the subplots
I tried to implement a fix suggested in the following link, however, could not make it work:
how do I make a single legend for many subplots with matplotlib?
Any help in this regard will be highly appreciated.
If I understand correctly, you need to tell plt.legend what to put as legends... at this point it is being loaded empty. What you get must be from another source. I have quickly the following, and of course when I run fig.legend as you do I get nothing.
import numpy as np
import matplotlib.pyplot as plt
fig = plt.figure()
ax1 = fig.add_axes([0.1, 0.1, 0.4, 0.7])
ax2 = fig.add_axes([0.55, 0.1, 0.4, 0.7])
x = np.arange(0.0, 2.0, 0.02)
y1 = np.sin(2*np.pi*x)
y2 = np.exp(-x)
l1, l2 = ax1.plot(x, y1, 'rs-', x, y2, 'go')
y3 = np.sin(4*np.pi*x)
y4 = np.exp(-2*x)
l3, l4 = ax2.plot(x, y3, 'yd-', x, y4, 'k^')
fig.legend(loc='upper right', fontsize='x-large')
#fig.legend((l1, l2), ('Line 1', 'Line 2'), 'upper left')
#fig.legend((l3, l4), ('Line 3', 'Line 4'), 'upper right')
plt.show()
I'd suggest doing one by one, and then applying for all.
It is useful to work with the axes directly (ax in your case) when when working with subplots. So if you set up two plots in a figure and only wish to have a legend in your second plot:
t = np.linspace(0, 10, 100)
plt.figure()
ax1 = plt.subplot(2, 1, 1)
ax1.plot(t, t * t)
ax2 = plt.subplot(2, 1, 2)
ax2.plot(t, t * t * t)
ax2.legend('Cubic Function')
Note that when creating the legend, I am doing so on ax2 as opposed to plt. If you wish to create a second legend for the first subplot, you can do so in the same way but on ax1.

Logarithmic multi-sequenz plot with equal bar widths

I have something like
import matplotlib.pyplot as plt
import numpy as np
a=[0.05, 0.1, 0.2, 1, 2, 3]
plt.hist((a*2, a*3), bins=[0, 0.1, 1, 10])
plt.gca().set_xscale("symlog", linthreshx=0.1)
plt.show()
which gives me the following plot:
As one can see, the bar width is not equal. In the linear part (from 0 to 0.1), everything is find, but after this, the bar width is still in linear scale, while the axis is in logarithmic scale, giving me uneven widths for bars and spaces in between (the tick is not in the middle of the bars).
Is there any way to correct this?
Inspired by https://stackoverflow.com/a/30555229/635387 I came up with the following solution:
import matplotlib.pyplot as plt
import numpy as np
d=[0.05, 0.1, 0.2, 1, 2, 3]
def LogHistPlot(data, bins):
totalWidth=0.8
colors=("b", "r", "g")
for i, d in enumerate(data):
heights = np.histogram(d, bins)[0]
width=1/len(data)*totalWidth
left=np.array(range(len(heights))) + i*width
plt.bar(left, heights, width, color=colors[i], label=i)
plt.xticks(range(len(bins)), bins)
plt.legend(loc='best')
LogHistPlot((d*2, d*3, d*4), [0, 0.1, 1, 10])
plt.show()
Which produces this plot:
The basic idea is to drop the plt.hist function, compute the histogram by numpy and plot it with plt.bar. Than, you can easily use a linear x-axis, which makes the bar width calculation trivial. Lastly, the ticks are replaced by the bin edges, resulting in the logarithmic scale. And you don't even have to deal with the symlog linear/logarithmic botchery anymore.
You could use histtype='stepfilled' if you are okay with a plot where the data sets are plotted one behind the other. Of course, you'll need to carefully choose colors with alpha values, so that all your data can still be seen...
a = [0.05, 0.1, 0.2, 1, 2, 3] * 2
b = [0.05, 0.05, 0.05, 0.15, 0.15, 2]
colors = [(0.2, 0.2, 0.9, 0.5), (0.9, 0.2, 0.2, 0.5)] # RGBA tuples
plt.hist((a, b), bins=[0, 0.1, 1, 10], histtype='stepfilled', color=colors)
plt.gca().set_xscale("symlog", linthreshx=0.1)
plt.show()
I've changed your data slightly for a better illustration. This gives me:
For some reason the overlap color seems to be going wrong (matplotlib 1.3.1 with Python 3.4.0; Is this a bug?), but it's one possible solution/alternative to your problem.
Okay, I found out the real problem: when you create the histogram with those bin-edge settings, the histogram creates bars which have equal size, and equal outside-spacing on the non-log scale.
To demonstrate, here's a zoomed-in version of the plot in the question, but in non-log scale:
Notice how the first two bars are centered around (0 + 0.1) / 2 = 0.05, with a gap of 0.1 / 10 = 0.01 at the edges, while the next two bars are centered around (0.1 + 1.0) / 2 = 0.55, with a gap of 1.1 / 10 = 0.11 at either edge.
When converting things to log scale, bar widths and edge widths all go for a huge toss. This is compounded further by the fact that you have a linear scale from 0 to 0.1, after which things become log-scale.
I know no way of fixing this, other than to do everything manually. I've used the geometric means of the bin-edges in order to compute what the bar edges and bar widths should be. Note that this piece of code will work only for two datasets. If you have more datasets, you'll need to have some function that fills in the bin-edges with a geometric series appropriately.
import numpy as np
import matplotlib.pyplot as plt
def geometric_means(a):
"""Return pairwise geometric means of adjacent elements."""
return np.sqrt(a[1:] * a[:-1])
a = [0.05, 0.1, 0.2, 1, 2, 3] * 2
b = [0.05, 0.1, 0.2, 1, 2, 3] * 3
# Find frequencies
bins = np.array([0, 0.1, 1, 10])
a_hist = np.histogram(a, bins=bins)[0]
b_hist = np.histogram(b, bins=bins)[0]
# Find log-scale mid-points for bar-edges
mid_vals = np.hstack((np.array([0.05,]), geometric_means(bins[1:])))
# Compute bar left-edges, and bar widths
a_x = np.empty(mid_vals.size * 2)
a_x = bins[:-1]
a_widths = mid_vals - bins[:-1]
b_x = np.empty(mid_vals.size * 2)
b_x = mid_vals
b_widths = bins[1:] - mid_vals
plt.bar(a_x, a_hist, width=a_widths, color='b')
plt.bar(b_x, b_hist, width=b_widths, color='g')
plt.gca().set_xscale("symlog", linthreshx=0.1)
plt.show()
And the final result:
Sorry, but the neat gaps between the bars get killed. Again, this can be fixed by doing the appropriate geometric interpolation, so that everything is linear on log-scale.
Just in case someone stumbles upon this problem:
This solution looks much more like the way it should be
plotting a histogram on a Log scale with Matplotlib