matplotlib subplots last plot disturbs log scale - python

I am making a matplotlib figure with a 2x2 dimension where x- and y-axis are shared, and then loop over the different axes to plot in them. I'm plotting variant data per sample, and it is possible that a sample doesn't have variant data, so then I want the plot to say "NA" in the middle of it.
import matplotlib.pyplot as plt
n_plots_per_fig = 4
nrows = 2
ncols = 2
fig, axs = plt.subplots(nrows, ncols, sharex="all", sharey="all", figsize=(8, 6))
axs = axs.ravel()
for i, ax in enumerate(axs):
x = [1, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3] # example values, but this list CAN be empty
bins = 3 # example bins
if x:
ax.hist(x, bins=bins) # plot the hist
ax.set_yscale("log")
ax.set_title(str(i), fontsize="medium")
else:
ax.set_title(str(i), fontsize="medium")
ax.text(0.5, 0.5, 'NA', ha='center', va='center', transform=ax.transAxes)
fig.show()
This works in almost every case; example of wanted output:
However, only if the last plot in the figure doesn't have any data, then this disturbs the log scale. Example code that triggers this:
import matplotlib.pyplot as plt
n_plots_per_fig = 4
nrows = 2
ncols = 2
fig, axs = plt.subplots(nrows, ncols, sharex="all", sharey="all", figsize=(8, 6))
axs = axs.ravel()
for i, ax in enumerate(axs):
x = [1, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3]
bins = 3
if i == n_plots_per_fig-1: # this will distort the log scale
ax.set_title(str(i), fontsize="medium")
ax.text(0.5, 0.5, 'NA', ha='center', va='center', transform=ax.transAxes)
elif x:
ax.hist(x, bins=bins) # plot the hist
ax.set_yscale("log")
ax.set_title(str(i), fontsize="medium")
else:
ax.set_title(str(i), fontsize="medium")
ax.text(0.5, 0.5, 'NA', ha='center', va='center', transform=ax.transAxes)
fig.show()
The log scale is now set to really low values, and this is not what I want. I've tried several things to fix this, like unsharing the y-axes for the plot that doesn't have any data [ax.get_shared_y_axes().remove(axis) for axis in axs] or hiding the plot ax.set_visible(False), but none of this works. The one thing that does work is removing the axes from the plot with ax.remove(), but since this is the bottom most sample, this also removes the values for the x ticks for that column:
And besides that, I would still like the name of the sample that didn't have any data to be visible in the axes (and the "NA" text), and removing the axes doesn't allow this.
Any ideas on a fix?
Edit: I simplified my example.

You can set the limits manually with ax.set_xlim() / ax.set_ylim().
Note, that if you share the axes it does not matter on which subplot you call those functions. For example:
axs[-1][-1].set_ylim(1e0, 1e2)
If you do not know the limits before, you can infer it from the other plots:
x = np.random.random(100)
bins = 10
if bins != 0:
...
yy, xx = np.histogram(x, bins=bins)
ylim = yy.min(), yy.max()
xlim = xx.min(), xx.max()
else:
ax.set_xlim(xlim)
ax.set_ylim(ylim)

Related

Remove empty space from matplotlib bar plot

I plot two bar plots to the same ax, where the x axis contains values from 0 downwards. However, I have significant gaps in my data (like from 0 to -15), and would like to remove the empty space, so that the axis goes from 0 to -15 with no space in between - show on this image:
Ideally I would like to do this for all gaps. I have tried both plt.axis('tight') and fig.tight_layout(), but neither of them have worked.
Edit: sample code for a small example
keys = [0, -15, -16, -17]
values = [3, 5, 2, 1]
fig, ax = plt.subplots(ncols=1)
fig.tight_layout()
ax.bar(keys, values, 0.8, color='g', align='center')
ax.set_xticks(keys)
plt.setp(ax.xaxis.get_majorticklabels(), rotation=90 )
The easiest way to resolve the issue is plot values against an x that is a range corresponding to the len of keys, and then change the xticklabels.
import matplotlib.pyplot as plt
keys = [0, -15, -16, -17]
values = [3, 5, 2, 1]
fig, ax = plt.subplots()
# create the xticks locations
x = range(len(keys))
ax.bar(x, values, 0.8, color='g', align='center')
# set the ticks and labels
ax.set_xticks(x)
_ = ax.set_xticklabels(keys)
Sorting
keys = [0, -15, -16, -17]
values = [3, 5, 2, 1]
# zip, sort and unpack
keys, values = zip(*sorted(zip(keys, values)))
fig, ax = plt.subplots()
# create the xticks locations
x = range(len(keys))
ax.bar(x, values, 0.8, color='g', align='center')
# set the ticks and labels
ax.set_xticks(x)
_ = ax.set_xticklabels(keys)

How to index a Matplotlib subplot

Im trying to plot two piecharts together. I have been reading the Matplotlib documentation https://matplotlib.org/stable/gallery/pie_and_polar_charts/pie_demo2.htmland cannot see what im doing wrong. I'm getting an indexing error in line 13 (patches = axs[1,1].pie...)
The code worked until I started using the axs[1,1] etc and tried to have the subplots.
Code
import matplotlib.pyplot as plt
from matplotlib import rcParams
print('\n'*10)
# Make figure and axes
fig, axs = plt.subplots(1,2)
# Pie chart, where the slices will be ordered and plotted counter-clockwise:
labels = 'Alpha', 'Beta', 'Gamma', 'Phi', 'Theta'
sizes = [3, 6, 2, 3, 10]
explode = (0, 0.1, 0, 0, 0) # only "explode" the 2nd slice (i.e. 'Hogs')
patches = axs[1,1].pie(sizes, explode=explode, labels=labels, autopct='%1.1f%%',
shadow=True, startangle=90)[0]
#patches[2].set_hatch('\\\\') # Pie slice #0 hatched.
axs[1,1].axis('equal') # Equal aspect ratio ensures that pie is drawn as a circle.
plt.title("My title", fontsize=14, fontweight='bold', size=16, y=1.02)
# Pie chart 2
labels = 'Alpha', 'Beta', 'Gamma', 'Phi', 'Theta'
sizes = [3, 6, 2, 3, 10]
explode = (0, 0.1, 0, 0, 0) # only "explode" the 2nd slice (i.e. 'Hogs')
patches = axs[1,2].pie(sizes, explode=explode, labels=labels, autopct='%1.1f%%',
shadow=True, startangle=90)[0]
patches[2].set_hatch('\\\\') # Pie slice #0 hatched.
axs[1,2].axis('equal') # Equal aspect ratio ensures that pie is drawn as a circle.
plt.title("My title", fontsize=14, fontweight='bold', size=16, y=1.02)
plt.show()
Traceback
Traceback (most recent call last):
File "/Users/.../Desktop/WORK/time_1.py", line 13, in <module>
patches = axs[1,1].pie(sizes, explode=explode, labels=labels, autopct='%1.1f%%',
IndexError: too many indices for array: array is 1-dimensional, but 2 were indexed
Array axs is 1-dimensional, change axs[1,1] and axs[1,2] to axs[0] and axs[1], then your code will work.
From matplotlib documentation.
# using the variable ax for single a Axes
fig, ax = plt.subplots()
# using the variable axs for multiple Axes
fig, axs = plt.subplots(2, 2)
# using tuple unpacking for multiple Axes
fig, (ax1, ax2) = plt.subplots(1, 2)
fig, ((ax1, ax2), (ax3, ax4)) = plt.subplots(2, 2)
So your axs is just a numpy array of shape (2,).
changing the index should do the trick.
Change axs[1,1] --> axs[0] , axs[1,2]--> axs[1]
The problem is that Matplotlib squeezes the axs array into a 1D shape if there is only one row or only one column of subplots. Fortunately, this inconsistent bahaviour can be disabled by passing the squeeze argument:
fig, axs = plt.subplots(1, 2, squeeze=False)
And then you can just normally index into it with axs[0,0] and axs[0,1], like you would if there were multiple rows of subplots.
I would recommend to always pass squeeze=False, so that the behaviour is the same regardless of how many rows there are and automated plotting scripts don't need to come up with special cases for single-row plots (or else risk cryptic errors if somebody later on wants to generate a plot that happens to have only a single row).

Matplotlib: Center tick-labels between subplots

By default, tick-labels are aligned on the subplot axis they belong to.
Is it possible to align the labels so they are centered between two subplots, instead?
import numpy as np
import matplotlib.pyplot as plt
data = [7, 2, 3, 0]
diff = [d - data[0] for d in data]
y = np.arange(len(data))
ax1 = plt.subplot(1, 2, 1)
ax1.barh(y, diff)
ax1.set_yticks(y + 0.4)
ax1.yaxis.set_major_formatter(matplotlib.ticker.NullFormatter())
ax2 = plt.subplot(1, 2, 2)
ax2.barh(y, data)
ax2.set_yticks(y + 0.4)
ax2.set_yticklabels(['reference', 'something', 'something else', 'nothing', ])
plt.tight_layout()
plt.show()
Here is a working, but not very convenient way of doing so. You can provide a position keyword when setting the xticklabels. This allows you to use a negative offset in axes coordinates. If you set the position of the axes, and the spacing between them manually, you can calculate what this negative offset needs to be for the labels to be exactly in the center between the two axes.
Given your example data:
fig = plt.figure(figsize=(10, 2), facecolor='w')
fig.subplots_adjust(wspace=0.2)
ax1 = fig.add_axes([0.0, 0, 0.4, 1])
ax2 = fig.add_axes([0.6, 0, 0.4, 1])
ax1.barh(y, diff, align='center')
ax1.set_yticks(y)
ax1.yaxis.set_major_formatter(matplotlib.ticker.NullFormatter())
ax2.barh(y, data, align='center')
ax2.set_yticks(y)
ax2.set_yticklabels(['reference', 'something', 'something else', 'nothing', ],
ha='center', position=(-0.25, 0))
The axes both have a width of 0.4 in figure coordinates, and they are spaced with 0.2. That means the labels would have to be at 0.5 in figure coordinates. Since the second axes starts at 0.6, it would need an offset in figure coordinates of -0.1. Unfortunately the position should be given in axes coordinates. The axes is 0.4 wide, so a quarter of the axes width is 0.1 in figure coordinates. That means specifying an offset of a negative quarter, -0.25, would place the labels right between the two axes. I hope that makes sense.....
Note that i have center the yticklabels with ha='center'. And also centered your bars, so you dont have to specify the offset anymore when setting the ticks.
edit:
You could do it automatically by reading the position of both axes.
def center_ylabels(ax1, ax2):
pos2 = ax2.get_position()
right = pos2.bounds[0]
pos1 = ax1.get_position()
left = pos1.bounds[0] + pos1.bounds[2]
offset = ((right - left) / pos2.bounds[2]) * -0.5
for yt in ax2.get_yticklabels():
yt.set_position((offset, yt.get_position()[1]))
yt.set_ha('center')
plt.setp(ax2.yaxis.get_major_ticks(), pad=0)
fig, (ax1, ax2) = plt.subplots(1,2, figsize=(10,2))
fig.subplots_adjust(wspace=0.5)
ax1.barh(y, diff, align='center')
ax1.set_yticks(y)
ax1.yaxis.set_major_formatter(matplotlib.ticker.NullFormatter())
ax2.barh(y, data, align='center')
ax2.set_yticks(y)
ax2.set_yticklabels(['reference', 'something', 'something else', 'nothing'])
center_ylabels(ax1, ax2)

Legend is outside the frame

import matplotlib.pyplot as plt
x, y = [1, 2, 3], [5, 7, 2]
fig = plt.figure()
ax = fig.add_subplot(111)
ax.plot(x, y)
fig.tight_layout() #растягивает графики на всё окно
leg = ax.legend(['legend'], bbox_to_anchor = (1.0, 0.5), loc='upper left',)
plt.show()
Legend is outside the frame. I see part of the legend, but I want to see all. How to do it?
This is what bbox_to_anchor does:
Users can specify any arbitrary location for the legend using the
*bbox_to_anchor* keyword argument. bbox_to_anchor can be an instance of BboxBase(or its derivatives) or a tuple of 2 or 4 floats. For
example:
loc = 'upper right', bbox_to_anchor = (0.5, 0.5)
will place the legend so that the upper right corner of the legend at
the center of the axes.
So play around with that tuple, for example try bbox_to_anchor = (0.05, 0.95). Or just leave it out altogether, and the legend will be in the upper left corner.
Edit: If you want the legend to be out of the subplot, you can try the following:
import matplotlib.pyplot as plt
x, y = [1, 2, 3], [5, 7, 2]
fig = plt.figure()
ax = fig.add_axes((0.2, 0.05, 0.75, 0.9))
ax.plot(x, y)
leg = ax.legend(['legend'], bbox_to_anchor = (0, 0.9))
plt.show()
You can tweak the numbers to fine-tune the positions.

Matplotlib different size subplots

I need to add two subplots to a figure. One subplot needs to be about three times as wide as the second (same height). I accomplished this using GridSpec and the colspan argument but I would like to do this using figure so I can save to PDF. I can adjust the first figure using the figsize argument in the constructor, but how do I change the size of the second plot?
As of matplotlib 3.6.0, width_ratios and height_ratios can now be passed directly as keyword arguments to plt.subplots and subplot_mosaic, as per What's new in Matplotlib 3.6.0 (Sep 15, 2022).
f, (a0, a1) = plt.subplots(1, 2, width_ratios=[3, 1])
f, (a0, a1, a2) = plt.subplots(3, 1, height_ratios=[1, 1, 3])
Another way is to use the subplots function and pass the width ratio with gridspec_kw
matplotlib Tutorial: Customizing Figure Layouts Using GridSpec and Other Functions
matplotlib.gridspec.GridSpec has available gridspect_kw options
import numpy as np
import matplotlib.pyplot as plt
# generate some data
x = np.arange(0, 10, 0.2)
y = np.sin(x)
# plot it
f, (a0, a1) = plt.subplots(1, 2, gridspec_kw={'width_ratios': [3, 1]})
a0.plot(x, y)
a1.plot(y, x)
f.tight_layout()
f.savefig('grid_figure.pdf')
Because the question is canonical, here is an example with vertical subplots.
# plot it
f, (a0, a1, a2) = plt.subplots(3, 1, gridspec_kw={'height_ratios': [1, 1, 3]})
a0.plot(x, y)
a1.plot(x, y)
a2.plot(x, y)
f.tight_layout()
You can use gridspec and figure:
import numpy as np
import matplotlib.pyplot as plt
from matplotlib import gridspec
# generate some data
x = np.arange(0, 10, 0.2)
y = np.sin(x)
# plot it
fig = plt.figure(figsize=(8, 6))
gs = gridspec.GridSpec(1, 2, width_ratios=[3, 1])
ax0 = plt.subplot(gs[0])
ax0.plot(x, y)
ax1 = plt.subplot(gs[1])
ax1.plot(y, x)
plt.tight_layout()
plt.savefig('grid_figure.pdf')
I used pyplot's axes object to manually adjust the sizes without using GridSpec:
import matplotlib.pyplot as plt
import numpy as np
x = np.arange(0, 10, 0.2)
y = np.sin(x)
# definitions for the axes
left, width = 0.07, 0.65
bottom, height = 0.1, .8
bottom_h = left_h = left+width+0.02
rect_cones = [left, bottom, width, height]
rect_box = [left_h, bottom, 0.17, height]
fig = plt.figure()
cones = plt.axes(rect_cones)
box = plt.axes(rect_box)
cones.plot(x, y)
box.plot(y, x)
plt.show()
Probably the simplest way is using subplot2grid, described in Customizing Location of Subplot Using GridSpec.
ax = plt.subplot2grid((2, 2), (0, 0))
is equal to
import matplotlib.gridspec as gridspec
gs = gridspec.GridSpec(2, 2)
ax = plt.subplot(gs[0, 0])
so bmu's example becomes:
import numpy as np
import matplotlib.pyplot as plt
# generate some data
x = np.arange(0, 10, 0.2)
y = np.sin(x)
# plot it
fig = plt.figure(figsize=(8, 6))
ax0 = plt.subplot2grid((1, 3), (0, 0), colspan=2)
ax0.plot(x, y)
ax1 = plt.subplot2grid((1, 3), (0, 2))
ax1.plot(y, x)
plt.tight_layout()
plt.savefig('grid_figure.pdf')
In a simple way, different size sub plotting can also be done without gridspec:
plt.figure(figsize=(12, 6))
ax1 = plt.subplot(2,3,1)
ax2 = plt.subplot(2,3,2)
ax3 = plt.subplot(2,3,3)
ax4 = plt.subplot(2,1,2)
axes = [ax1, ax2, ax3, ax4]
A nice way of doing this was added in matplotlib 3.3.0, subplot_mosaic.
You can make a nice layout using an "ASCII art" style.
For example
fig, axes = plt.subplot_mosaic("ABC;DDD")
will give you three axes on the top row and one spanning the full width on the bottom row like below
A nice thing about this method is that the axes returned from the function is a dictionary with the names you define, making it easier to keep track of what is what e.g.
axes["A"].plot([1, 2, 3], [1, 2, 3])
You can also pass a list of lists to subplot_mosaic if you want to use longer names
fig, axes = plt.subplot_mosaic(
[["top left", "top centre", "top right"],
["bottom row", "bottom row", "bottom row"]]
)
axes["top left"].plot([1, 2, 3], [1, 2, 3])
will produce the same figure

Categories

Resources