Matplotlib: Multiple plots with same layout (no automatic layout) - matplotlib

I am trying to make several pie charts that I can then transition between in a presentation. For this, it would be very useful for the automatic layouting to... get out of the way. The problem is that whenever I change a label, the whole plot moves around on the canvas so that it fits perfectly. I'd like the plot to stay centered, so it occupies the same area every time. I have tried adding center=(0,0) to ax.pie(), but to no avail.
Two examples:
Image smaller, left
Image larger, right
Instead of that effect, I'd like the pie chart to be in the middle of the canvas and have the same size in both cases (and I'd then manually make sure that the labels are on canvas by setting large margins).
The code I use to generate these two images is:
import matplotlib.pyplot as plt
import numpy as np
# Draw labels, from
# https://matplotlib.org/3.2.2/gallery/pie_and_polar_charts/pie_and_donut_labels.html#sphx-glr-gallery-pie-and-polar-charts-pie-and-donut-labels-py
def make_labels(ax, wedges, labs):
bbox_props = dict(boxstyle="square,pad=0.3", fc="w", ec="k", lw=0.72)
kw = dict(arrowprops=dict(arrowstyle="-"),
bbox=bbox_props,
zorder=0, va="center")
for i, p in enumerate(wedges):
if p.theta2-p.theta1 < 5:
continue
ang = (p.theta2 - p.theta1) / 2. + p.theta1
y = np.sin(np.deg2rad(ang))
x = np.cos(np.deg2rad(ang))
horizontalalignment = {-1: "right", 1: "left"}[int(np.sign(x))]
connectionstyle = "angle,angleA=0,angleB={}".format(ang)
kw["arrowprops"].update({"connectionstyle": connectionstyle})
ax.annotate(labs[i], xy=(x, y),
xytext=(1.1*x,1.1*y),
horizontalalignment=horizontalalignment, **kw)
kw=dict(autoscale_on=False, in_layout=False, xmargin=1, ymargin=1)
fig, ax = plt.subplots(figsize=(3, 3), dpi=100, subplot_kw=kw)
wedges, texts = ax.pie(x=[1,2,3], radius=1,
wedgeprops=dict(width=1),
pctdistance=0.7,
startangle=90,
textprops=dict(fontsize=8),
center=(0, 0))
make_labels(ax, wedges, ["long text", "b", "c"])
#make_labels(ax, wedges, ["a", "b", "long text"])
plt.show()
Thanks a lot in advance!

How are you saving your figures? It looks like you may be using savefig(..., bbox_inches='tight') which automatically resized the figure to include all the artists.
If I run your code with fig.savefig(..., bbox_inches=None), I get the following output

Related

Matplotlib sliders on multiple figures

I am writing a Python tool that needs several figures open at the same time, each one with its own widgets (sliders, for the most part). I don't need any interactions across the figures here. Each figure is independent of the other ones, with its own plot and its own sliders affecting only itself.
I can get Matplotlib sliders working fine on a single figure, but I can't get them to work on multiple figures concurrently. Only the sliders of the LAST figure to open are working. The other ones are unresponsive.
I recreated my problem with the simple code below, starting from the example in the Matplotlib.Slider doc. If I run it as-is, only the sliders for the second figure (amplitude) works. The other doesn't. If I invert the two function calls at the bottom, it's the other way around.
I've had no luck googling solutions or pointers. Any help would be much appreciated.
I'm on Python 3.9.12, btw. I can upload a requirements file if someone tries and cannot reproduce the issue. Thank you!
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.widgets import Slider
# The parametrized function to be plotted
def f(time, amplitude, frequency):
return amplitude * np.sin(2 * np.pi * frequency * time)
# Define initial parameters
init_amplitude = 5
init_frequency = 3
t = np.linspace(0, 1, 1000)
def create_first_fig():
# Create the figure and the line that we will manipulate
fig1, ax1 = plt.subplots()
line1, = ax1.plot(t, f(t, init_amplitude, init_frequency), lw=2, color='b')
ax1.title.set_text('First plot - interactive frequency')
ax1.set_xlabel('Time [s]')
# adjust the main plot to make room for the sliders
fig1.subplots_adjust(left=0.25, bottom=0.25)
# Make a horizontal slider to control the frequency.
axfreq = fig1.add_axes([0.25, 0.1, 0.65, 0.03])
freq_slider = Slider(
ax=axfreq,
label='Frequency [Hz]',
valmin=0,
valmax=30,
valinit=init_frequency,
)
# register the update function with each slider
freq_slider.on_changed(lambda val: update_first_fig(val, fig1, line1))
plt.draw()
plt.pause(0.1)
return fig1
# The function to be called anytime a slider's value changes
def update_first_fig(val, fig, line):
line.set_ydata(f(t, init_amplitude, val))
fig.canvas.draw_idle()
plt.pause(0.1)
def create_second_fig():
# Create the figure and the line that we will manipulate
fig2, ax2 = plt.subplots()
line2, = ax2.plot(t, f(t, init_amplitude, init_frequency), lw=2, color='r')
ax2.title.set_text('Second plot - interactive amplitude')
ax2.set_xlabel('Time [s]')
# adjust the main plot to make room for the sliders
fig2.subplots_adjust(left=0.25, bottom=0.25)
# Make a vertically oriented slider to control the amplitude
axamp = fig2.add_axes([0.1, 0.25, 0.0225, 0.63])
amp_slider = Slider(
ax=axamp,
label="Amplitude",
valmin=0,
valmax=10,
valinit=init_amplitude,
orientation="vertical",
)
# register the update function with each slider
amp_slider.on_changed(lambda val: update_second_fig(val, fig2, line2))
plt.draw()
plt.pause(0.1)
return fig2
# The function to be called anytime a slider's value changes
def update_second_fig(val, fig, line):
line.set_ydata(f(t, val, init_frequency))
fig.canvas.draw_idle()
plt.pause(0.1)
figure1 = create_first_fig()
figure2 = create_second_fig()
plt.show()
I would expect the slider in both figures to work the way it does when I only open the corresponding figure. So far it's only the slider in the figure that's created last that works.
Edit in case someone else looks at this: see Yulia V's answer below. It works perfectly, including in my initial application. The site doesn't let me upvote it because I am too new on here, but it's a perfect solution to my problem. Thanks Yulia V!
You need to save the references to sliders as variables to make it work. No idea why, but this is how matplotlib works.
Specifically, in your functions, you need to have
return freq_slider, fig1
...
return amp_slider, fig2
instead of
return fig1
...
return fig2
and in the main script,
freq_slider, figure1 = create_first_fig()
amp_slider, figure2 = create_second_fig()
instead of
figure1 = create_first_fig()
figure2 = create_second_fig()
Just to illustrate my comment below #Yulia V's answer, it works too if we store the sliders as an attribute of the figure instead of returning them:
def create_first_fig():
...
fig1._slider = freq_slider
...
return fig1
def create_first_fig():
...
fig2._slider = amp_slider
...
return fig2
...
figure1 = create_first_fig()
figure2 = create_second_fig()

Some matplotlib colorbars disappear when colorbar axes are moved

I am using the following lines of python code to create a figure with multiple subplots in a Jupiter notebook and attempting to add colorbars to some of the plots. The following lines are 1 of 7 sections copied and pasted with adjustments to GridSpec, variables, labels and axes handles made for each:
fig = plt.figure(figsize=(20,20))
gs = gridspec.GridSpec(21, 13)
...
if i >= 1:
ax3 = plt.subplot(gs[6:9, 3*i+1:3*i+4],projection=ccrs.Robinson())
else:
ax3 = plt.subplot(gs[6:9, 3*i:3*i+3],projection=ccrs.Robinson())
if i == 0:
cs3 = ax3.contourf(Lon,lat,cldhgh.squeeze(),12,transform=ccrs.PlateCarree(),cmap='gist_gray',vmin=0,vmax=1)
ax3.coastlines()
Cticks=np.around(np.linspace(0,1,6),decimals=1)
Cbar_ax3 = fig.add_axes([0.3,0.58,0.01,0.10])
cb3 = fig.colorbar(cs3, spacing='proportional',orientation='vertical',cax=Cbar_ax3,ticks=Cticks)
#cb2.set_ticklabels(Cticks.astype(int).astype(str),fontsize=7)
cb3.set_ticklabels(Cticks.astype(str),fontsize=12)
cb3.set_label('High Cloud Fraction',fontsize=10)
else:
cs3 = ax3.contourf(Lon,lat,delta_cldhgh,61,transform=ccrs.PlateCarree(),cmap='BrBG',vmin=-0.2,vmax=0.2)
c3 = ax3.contour(Lon,lat,cldhgh.squeeze(),12,vmin=0,vmax=1,colors='black',linewidths=0.5)
ax3.coastlines()
if i == 1:
cticks=np.around(np.linspace(-0.2,0.2,5),decimals=1)
cbar_ax = fig.add_axes([1.02,0.58,0.01,0.10])
ax3.set_ylabel('Hybrid Sigma-Pressure level (mb)',fontsize=12)
#cb = fig.colorbar(cs, spacing='proportional',orientation='vertical',cax=cbar_ax,ticks=cticks)
cb3 = fig.colorbar(mappable=None, norm=Normalize(vmin=-0.2,vmax=0.2), cmap='BrBG',spacing='proportional',orientation='vertical',cax=cbar_ax,ticks=cticks)
cb3.set_ticklabels(cticks.astype(str),fontsize=12)
#cb2.set_ticklabels(cticks.astype(int).astype(str),fontsize=10)
cb3.set_label('Cloud Fraction Difference',fontsize=10)
...
plt.suptitle('Comparison of mappables of Background Climate States',fontsize=24,y=1.01)
#fig.text(-0.04, 0.5, 'Sigma Pressure Level (mb)', va='center', rotation='vertical')
fig.tight_layout(pad=0.2)
plt.show()
fig.savefig(figure_path+'Reference_Climate_Comparison_of_Mappables.pdf',bbox_inches='tight')
I am able to almost do this successfully, except the original guess I made for the x displacement of my colorbars on the left side of the figure was too large:
To fix this I simply adjusted the first index of each subplot's "Cbar_ax" variable to be slightly smaller (e.g. from 0.3 to 0.25):
Cbar_ax3 = fig.add_axes([0.25,0.58,0.01,0.10])
The adjustment works for some subplots, but for others the colorbars all but vanish:
I have no idea how to solve this problem. I can make the colorbars appear using plt.colorbar() instead of fig.colorbar() without an colorbar axes designation, but the subplots themselves are not a consistent size with the rest of the figure (since plt.colorbar steals axes space from it's parent axes by default). What am I not seeing here? Why do some of these colorbars disappear when I move them?

subplots_adjust moves axes unpredictably?

I'm working on a python module that creates a matplotlib figure with an on_resize listener. The listener forces the height of the lower axes to a specific number of pixels (rather than scaling relative to figure size). It works. However, if (in matplotlib interactive mode) after creating the plot the user calls fig.subplots_adjust() it messes up subplot sizes. Here's a radically simplified version of what the module does:
import matplotlib.pyplot as plt
plt.ion()
def make_plot():
fig = plt.figure()
gs = plt.GridSpec(10, 1, figure=fig)
ax_upper = fig.add_subplot(gs[:-1])
ax_lower = fig.add_subplot(gs[-1])
ax_upper.plot([0, 1])
ax_lower.plot([0, 1])
fig.canvas.mpl_connect('resize_event', on_resize)
return fig
def on_resize(event):
fig = event.canvas.figure
# get the current position
ax_lower_pos = list(fig.axes[1].get_position().bounds) # L,B,W,H
# compute desired height in figure-relative coords
desired_height_px = 40
xform = fig.transFigure.inverted()
desired_height_rel = xform.transform([0, desired_height_px])[1]
# set the new height
ax_lower_pos[-1] = desired_height_rel
fig.axes[1].set_position(ax_lower_pos)
# adjust ax_upper accordingly
ax_lower_top = fig.axes[1].get_position().extents[-1] # L,B,R,T
ax_upper_pos = list(fig.axes[0].get_position().bounds) # L,B,W,H
# new bottom
new_upper_bottom = ax_lower_top + desired_height_rel
ax_upper_pos[1] = new_upper_bottom
# new height
ax_upper_top = fig.axes[0].get_position().extents[-1] # L,B,R,T
new_upper_height = ax_upper_top - new_upper_bottom
ax_upper_pos[-1] = new_upper_height
# set the new position
fig.axes[0].set_position(ax_upper_pos)
fig.canvas.draw()
Here's the output if the user calls fig = make_plot():
Now if the user calls fig.subplots_adjust, the bottom axis is squished and the space between bottom and top axes is even more squished (the on_resize listener had set them both to 40px):
fig.subplots_adjust(top=0.7)
At this point, grabbing the corner of the window and dragging even a tiny bit is enough to trigger the on_resize listener and restore what I want (fixed pixel height for bottom axes and space between axes) while keeping the newly-added wide top margin intact:
How can I get that result without having to manually trigger a resize event? As far as I can tell, subplots_adjust does not fire off any events that I could listen for.
I think the problem lies in ax.update_params() updating the axes position with a figbox taken from the underlying subplotspec (which as far as I can tell doesn't get updated after initial figure creation?). (note: update_params is called from within subplots_adjust, see here).
The underlying problem seems to be to make an axes with a specific height in pixels. An easy solution to this is to use mpl_toolkits.axes_grid1's make_axes_locatable.
This allows to get rid of any callback and hence of the complete problem of the race condition in the events.
A note: The plot seems to be part of a bigger library. Since it is always nice not to patronize the users of such packages, one would usually allow them to specify the axes to plot to, such that they can put the plot into a bigger figure with other elements. The below solution makes this particularly easy.
Of course, also calling plt.subplots_adjust is still possible at any time.
import matplotlib.pyplot as plt
from mpl_toolkits.axes_grid1.axes_divider import make_axes_locatable
desired_height_px = 40 #pixel
def make_plot(ax=None):
if not ax:
fig, ax = plt.subplots()
else:
fig = ax.figure
div = make_axes_locatable(ax)
cax = div.append_axes("bottom", desired_height_px/fig.dpi, pad=0.25)
sc1 = ax.scatter([2,1,3], [2,3,1], c=[1,2,3])
sc2 = cax.scatter([3,2,1],[2,3,1], c=[3,1,2])
return fig, ax, cax, (sc1, sc2)
fig, (ax1, ax2) = plt.subplots(1,2)
make_plot(ax=ax1)
#user plot on ax2
ax2.plot([1,3])
fig.subplots_adjust(top=0.7)
plt.show()

Pandas, Bar Chart Annotations

How to properly give Annotations to Pandas Bar Charts?
I'm following Bar Chart Annotations with Pandas and MPL, but somehow I can't make it into my own code -- this is as far as I can go. What's wrong?
I've also found the following code from here:
def autolabel(rects):
# attach some text labels
for rect in rects:
height = rect.get_height()
ax.text(rect.get_x() + rect.get_width()/2., 1.05*height,
'%d' % int(height),
ha='center', va='bottom')
autolabel(rects1)
autolabel(rects2)
But I don't how to apply that to my code either. Please help.
UPDATE:
Thank you #CT Zhu, for the answer. However, in your horizontal bars, you are still placing the text on top of bars, but I need the text show up within or along them, like this from my referenced article,
where s/he says,
"I am very parital to horizontal bar charts, as I really think they are easier to read, however, I understand that a lot of people would rather see this chart implemented in a regular bar chart. So, here is the code to do that; you will notice that a few things have changed in order to create the annotation"*
It appears your autolabel function is expecting a list of patches, sssuming your plot only those bars as its patches, we could do:
df = pd.DataFrame({'score':np.random.randn(6),
'person':[x*3 for x in list('ABCDEF')]})
def autolabel(rects):
x_pos = [rect.get_x() + rect.get_width()/2. for rect in rects]
y_pos = [rect.get_y() + 1.05*rect.get_height() for rect in rects]
#if height constant: hbars, vbars otherwise
if (np.diff([plt.getp(item, 'width') for item in rects])==0).all():
scores = [plt.getp(item, 'height') for item in rects]
else:
scores = [plt.getp(item, 'width') for item in rects]
# attach some text labels
for rect, x, y, s in zip(rects, x_pos, y_pos, scores):
ax.text(x,
y,
'%s'%s,
ha='center', va='bottom')
ax = df.set_index(['person']).plot(kind='barh', figsize=(10,7),
color=['dodgerblue', 'slategray'], fontsize=13)
ax.set_alpha(0.8)
ax.set_title("BarH")#,fontsize=18)
autolabel(ax.patches)
ax = df.set_index(['person']).plot(kind='bar', figsize=(10,7),
color=['dodgerblue', 'slategray'], fontsize=13)
ax.set_alpha(0.8)
ax.set_title("Bar")#,fontsize=18)
autolabel(ax.patches)

Graphics issues when combining matplotlib widgets: Spanselector, cursor, fill_between:

I have found minor graphical issues while using the spanselector, cursor and fill_between widgets, which I would like to share with you.
All of them, can be experienced in this code (which I took from the matplolib example)
"""
The SpanSelector is a mouse widget to select a xmin/xmax range and plot the
detail view of the selected region in the lower axes
"""
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.widgets import SpanSelector
import matplotlib.widgets as widgets
Fig = plt.figure(figsize=(8,6))
Fig.set_facecolor('w')
Fig.set
Ax = Fig.add_subplot(211)
x = np.arange(0.0, 5.0, 0.01)
y = np.sin(2*np.pi*x) + 0.5*np.random.randn(len(x))
Ax.plot(x, y, '-')
Ax.set_ylim(-2,2)
Ax.set_title('Press left mouse button and drag to test')
RegionIndices = []
ax2 = Fig.add_subplot(212)
line2, = ax2.plot(x, y, '-')
def onselect(xmin, xmax):
if len(RegionIndices) == 2:
Ax.fill_between(x[:], 0.0, y[:],facecolor='White',alpha=1)
del RegionIndices[:]
indmin, indmax = np.searchsorted(x, (xmin, xmax))
indmax = min(len(x)-1, indmax)
Ax.fill_between(x[indmin:indmax], 0.0, y[indmin:indmax],facecolor='Blue',alpha=0.30)
thisx = x[indmin:indmax]
thisy = y[indmin:indmax]
line2.set_data(thisx, thisy)
ax2.set_xlim(thisx[0], thisx[-1])
ax2.set_ylim(thisy.min(), thisy.max())
Fig.canvas.draw()
RegionIndices.append(xmin)
RegionIndices.append(xmax)
# set useblit True on gtkagg for enhanced performance
span = SpanSelector(Ax, onselect, 'horizontal', useblit = True,rectprops=dict(alpha=0.5, facecolor='purple') )
cursor = widgets.Cursor(Ax, color="red", linewidth = 1, useblit = True)
plt.show()
I wonder if there is some way to avoid these two small issues:
1) You can see that when you select a region the spanselector box (purple) glitches. In this code the effect is barely noticeable but on plots with many lines is quite annoying (I have tried all the trueblit combinations to not effect)
2) In this code when you select a region, the area in the upper plot between the line and the horizontal axis is filled in blue. When you select a new region the old area is filled in white (to clear it) and the new one is filled with blue again. However, when I do that the line plotted, as well as, the horizontal axis, become thicker... Is there a way to clear such a region (generated with fill_between) without this happening... Or is it necessary to replot the graph? Initially, I am against doing this since I have a well structured code and importing all the data again into the spanselector method seems a bit messy... Which is the right way in python to delete selected regions of a plot?
Any advice would be most welcome