Matplotlib - axvspan vs subplots - python

I'm writing a pythonic script for a coastal engineering application which should output, amongst other things, a figure with two subplots.
The problem is that I would like to shade a section of both subplots using plt.axvspan() but for some reason it only shades one of them.
Please find below an excerpt of the section of the code where I set up the plots as well as the figure that it's currently outputting (link after code).
Thanks for your help, and sorry if this is a rookie question (but it just happens that I am indeed a rookie in Python... and programming in general) but I couldn't find an answer for this anywhere else.
Feel free to add any comments to the code.
# PLOTTING
# now we generate a figure with the bathymetry vs required m50 and another figure with bathy vs Hs
#1. Generate plots
fig = plt.figure() # Generate Figure
ax = fig.add_subplot(211) # add the first plot to the figure.
depth = ax.plot(results[:,0],results[:,1]*-1,label="Depth [mDMD]") #plot the first set of data onto the first set of axis.
ax2 = ax.twinx() # generate a secondary vertical axis with the same horizontal axis as the first
m50 = ax2.plot(results[:,0],results[:,6],"r",label="M50 [kg]") # plot the second set of data onto the second vertical axis
ax3 = fig.add_subplot(212) # generate the second subplot
hs = ax3.plot(results[:,0],results[:,2],"g",label="Hs(m)")
#Now we want to find where breaking starts to occur so we shade it on the plot.
xBreakingDistance = results[numpy.argmax(breakingIndex),0]
# and now we plot a box from the origin to the depth of breaking.
plt.axvspan(0,xBreakingDistance,facecolor="b",alpha=0.1) # this box is called a span in matplotlib (also works for axhspan)
# and then we write BREAKING ZONE in the box we just created
yLimits = ax.get_ylim() # first we get the range of y being plotted
yMiddle = (float(yLimits[1])-float(yLimits[0])) / 2 + yLimits[0] # then we calculate the middle value in y (to center the text)
xMiddle = xBreakingDistance / 2 # and then the middle value in x (to center the text)
#now we write BREAKING ZONE in the center of the box.
ax.text(xMiddle,yMiddle,"BREAKING ZONE",fontweight="bold",rotation=90,verticalalignment="center",horizontalalignment="center")
#FIGURE FORMATTING
ax.set_xlabel("Distance [m]") # define x label
ax.set_ylabel("Depth [mDMD]") # define y label on the first vertical axis (ax)
ax2.set_ylabel("M50 [kg]") # define y label on the second vertical axis (ax2)
ax.grid() # show grid
ax3.set_xlabel("Distance[m]") #define x label
ax3.set_ylabel("Hs[m]") # define y label
ax3.grid()
plt.tight_layout() # minimize subplot labels overlapping
# generating a label on a plot with 2 vertical axis is not very intuitive. Normally we would just write ax.label(loc=0)
combined_plots = depth+m50 #first we need to combine the plots in a vector
combined_labels = [i.get_label() for i in combined_plots] # and then we combine the labels
ax.legend(combined_plots,combined_labels,loc=0) # and finally we plot the combined_labels of the combined_plots
plt.savefig("Required M50(kg) along the trench.png",dpi=1000)
plt.close(fig)
Output Figure:

By just calling plt.axvspan, you are telling matplotlib to create the axvspan on the currently active axes (i.e. in this case, the last one you created, ax3)
You need to plot the axvspan on both of the axes you would like for it to appear on. In this case, ax and ax3.
So, you could do:
ax.axvspan(0,xBreakingDistance,facecolor="b",alpha=0.1)
ax3.axvspan(0,xBreakingDistance,facecolor="b",alpha=0.1)
or in one line:
[this_ax.axvspan(0,xBreakingDistance,facecolor="b",alpha=0.1) for this_ax in [ax,ax3]]

It's difficult to analyze your code and not being able to reproduce it. I advise you to build a minimal example. In any case notice that you are calling "plt.axvspan(" which is general call to the library.
You need to specifically state that you want this in both "ax" and "ax2" (i think).
Also if you need more control consider using Patches (I don't know axvspan):
import matplotlib.pyplot as plt
import matplotlib.patches as patches
fig1 = plt.figure()
ax1 = fig1.add_subplot(111, aspect='equal')
ax1.add_patch(
patches.Rectangle(
(0.1, 0.1), # (x,y)
0.5, # width
0.5, # height
)
)
fig1.savefig('rect1.png', dpi=90, bbox_inches='tight')
See that call to "ax1" in the example? Just make something similar to yours. Or just add axvspan to each of your plots.

Related

Properly adding a second set of ticks to python matplotlib colorbar

I have a figure with three subplots. The top two subplots share a similar data range, while the bottom one shows data with a different data range. I'd like to use only one colorbar for the whole figure by having ticks for the top two subplots to the left of the colorbar and having ticks for the bottom subplot to the right of the colorbar (see fig bellow).
I have been able to do this using a dirty hack, namely by displaying two colorbars on top of each other and moving the ticks of one of them to the left. As an example I've modified this matplotlib example:
import matplotlib.pyplot as plt
import numpy as np
# Fixing random state for reproducibility
np.random.seed(19680801)
# create three subplots
fig, axes = plt.subplots(3)
# filling subplots with figures and safing the map of the first and third figure.
# fig 1-2 have a data range of 0 - 1
map12 =axes[0].imshow(np.random.random((100, 100)), cmap=plt.cm.BuPu_r)
axes[1].imshow(np.random.random((100, 100)), cmap=plt.cm.BuPu_r)
# figure 3 has a larger data range from 0 - 5
map3 = axes[2].imshow(np.random.random((100, 100))*5, cmap=plt.cm.BuPu_r)
# Create two axes for the colorbar on the same place.
# They have to be very slightly missplaced, else a warning will appear and only the second colorbar will show.
cax12 = plt.axes([0.85, 0.1, 0.075, 0.8])
cax3 = plt.axes([0.85, 0.100000000000001, 0.075, 0.8])
# plot the two colorbars
cbar12 = plt.colorbar(map12, cax=cax12, label='ticks for top two figs')
cbar3 = plt.colorbar(map3, cax=cax3, label='ticks for bottom fig')
# move ticks and label of second plot to the left
cbar12.ax.yaxis.set_ticks_position('left')
cbar12.ax.yaxis.set_label_position('left')
## display image
plt.show()
While I'm happy with the visual result, i think there has to be a better way to do this. One problem is that if you save it as vector graphic, you will end up with overlapping shapes. Also if you make a mistake with the colors of the lower colorbar you might not realize it because the colors are hidden, or it might give you a headache if you want to make the colorbar sightly transpartent for some reason. I therefore wonder how one would do this properly, or if this is not possible, if there is a better hack?
You can achieve the same result without drawing the second colorbar, you just need to create a new axes with the ticks to the right, and adjust the range of the y-axis to the range of data of your 3rd plot.
import matplotlib.pyplot as plt
import numpy as np
# Fixing random state for reproducibility
np.random.seed(19680801)
# create three subplots
fig, axes = plt.subplots(3)
# filling subplots with figures and safing the map of the first and third figure.
# fig 1-2 have a data range of 0 - 1
map12 =axes[0].imshow(np.random.random((100, 100)), cmap=plt.cm.BuPu_r)
axes[1].imshow(np.random.random((100, 100)), cmap=plt.cm.BuPu_r)
# figure 3 has a larger data range from 0 - 5
map3 = axes[2].imshow(np.random.random((100, 100))*5, cmap=plt.cm.BuPu_r)
# Create two axes for the colorbar on the same place.
cax12 = plt.axes([0.85, 0.1, 0.075, 0.8])
cax3 = cax12.twinx()
# plot first colorbar
cbar12 = plt.colorbar(map12, cax=cax12, label='ticks for top two figs')
# move ticks and label of colorbar to the left
cbar12.ax.yaxis.set_ticks_position('left')
cbar12.ax.yaxis.set_label_position('left')
# adjust limits of right axis to match data range of 3rd plot
cax3.set_ylim(0,5)
cax3.set_ylabel('ticks for bottom fig')
## display image
plt.show()
For some reason, the above answer did not work for me. I do not know why. What worked for me is as follows:
cax2 = fig.add_axes([<xposition>, <yposition>, <xlength>, <ylegth>])
cax21 = cax2.twinx()
cax2.set_ylabel('right-label',size=<right_lable_size>)
cax2.tick_params(labelsize=<right_tick_size>)
'''
These did not work for me
cbar21.ax.yaxis.set_ticks_position('left')
cbar21.ax.yaxis.set_label_position('left')
'''
# This worked.
cax21.yaxis.tick_left()
cax21.yaxis.label_position='left'
cax21.set_ylim(<minVal>,<maxVal>,<step>)
cax21.set_ylabel("left-label",size=<left_lable_size>)
cax21.tick_params(labelsize=<left_tick_size>)
Hopefully, this helps.

Make x-axes of all subplots same length on the page

I am new to matplotlib and trying to create and save plots from pandas dataframes via a loop. Each plot should have an identical x-axis, but different y-axis lengths and labels. I have no problem creating and saving the plots with different y-axis lengths and labels, but when I create the plots, matplotlib rescales the x-axis depending on how much space is needed for the y-axis labels on the left side of the figure.
These figures are for a technical report. I plan to place one on each page of the report and I would like to have all of the x-axes take up the same amount of space on the page.
Here is an MSPaint version of what I'm getting and what I'd like to get.
Hopefully this is enough code to help. I'm sure there are lots of non-optimal parts of this.
import pandas as pd
import matplotlib.pyplot as plt
import pylab as pl
from matplotlib import collections as mc
from matplotlib.lines import Line2D
import seaborn as sns
# elements for x-axis
start = -1600
end = 2001
interval = 200 # x-axis tick interval
xticks = [x for x in range(start, end, interval)] # create x ticks
# items needed for legend construction
lw_bins = [0,10,25,50,75,90,100] # bins for line width
lw_labels = [3,6,9,12,15,18] # line widths
def make_proxy(zvalue, scalar_mappable, **kwargs):
color = 'black'
return Line2D([0, 1], [0, 1], color=color, solid_capstyle='butt', **kwargs)
# generic image ID
img_path = r'C:\\Users\\user\\chart'
img_ID = 0
for line_subset in data:
# create line collection for this run through loop
lc = mc.LineCollection(line_subset)
# create plot and set properties
sns.set(style="ticks")
sns.set_context("notebook")
fig, ax = pl.subplots(figsize=(16, len(line_subset)*0.5)) # I want the height of the figure to change based on number of labels on y-axis
# Figure width should stay the same
ax.add_collection(lc)
ax.set_xlim(left=start, right=end)
ax.set_xticks(xticks)
ax.set_ylim(0, len(line_subset)+1)
ax.margins(0.05)
sns.despine(left=True)
ax.xaxis.set_ticks_position('bottom')
ax.set_yticks(line_subset['order'])
ax.set_yticklabels(line_subset['ylabel'])
ax.tick_params(axis='y', length=0)
# legend
proxies = [make_proxy(item, lc, linewidth=item) for item in lw_labels]
ax.legend(proxies, ['0-10%', '10-25%', '25-50%', '50-75%', '75-90%', '90-100%'], bbox_to_anchor=(1.05, 1.0),
loc=2, ncol=2, labelspacing=1.25, handlelength=4.0, handletextpad=0.5, markerfirst=False,
columnspacing=1.0)
# title
ax.text(0, len(line_subset)+2, s=str(img_ID), fontsize=20)
# save as .png images
plt.savefig(r'C:\\Users\\user\\Desktop\\chart' + str(img_ID) + '.png', dpi=300, bbox_inches='tight')
Unless you use an axes of specifically defined aspect ratio (like in an imshow plot or by calling .set_aspect("equal")), the space taken by the axes should only depend on the figure size along that direction and the spacings set to the figure.
You are therefore pretty much asking for the default behaviour and the only thing that prevents you from obtaining that is that you use bbox_inches='tight' in the savefig command.
bbox_inches='tight' will change the figure size! So don't use it and the axes will remain constant in size. `
Your figure size, defined like figsize=(16, len(line_subset)*0.5) seems to make sense according to what I understand from the question. So what remains is to make sure the axes inside the figure are the size you want them to be. You can do that by manually placing it using fig.add_axes
fig.add_axes([left, bottom, width, height])
where left, bottom, width, height are in figure coordinates ranging from 0 to 1. Or, you can adjust the spacings outside the subplot using subplots_adjust
plt.subplots_adjust(left, bottom, right, top)
To get matching x axis for the subplots (same x axis length for each subplot) , you need to share the x axis between subplots.
See the example here https://matplotlib.org/examples/pylab_examples/shared_axis_demo.html

Matplotlib: Sharing axes when having 3 graphs 2 at the left and 1 at the right

I have following graph:
However, I want that graphs 221 and 223 share the same x axis. I have the following code:
self.fig_part_1 = plt.figure()
self.plots_part_1 = [
plt.subplot(221),
plt.subplot(223),
plt.subplot(122),
]
How can I achieve that? In the end I do not want the numbers of axis x in plot 221 to be shown.
(This is mostly a comment to #H. Rev. but I post it as an "answer" to get nicer code formatting)
I think it is way better to just add the subplots manually, since as you implemented it now it will give two axes that you just throw away. They might even give problems with overlapping axis-ticks and a lot of confusion in general. I believe it is better to create the figure first, and then add axes one by one. This way also solves the problem by having to "update" the current figure with plt.figure(self.f.number) since you have direct access to e.g. fig_N
import matplotlib.pyplot as plt
fig1 = plt.figure()
# fig2 = plt.figure() # more figures are easily accessible
# fig3 = plt.figure() # more figures are easily accessible
ax11 = fig1.add_subplot(221) # add subplot into first position in a 2x2 grid (upper left)
ax12 = fig1.add_subplot(223, sharex=ax11) # add to third position in 2x2 grid (lower left) and sharex with ax11
ax13 = fig1.add_subplot(122) # add subplot to cover both upper and lower right, in a 2x2 grid. This is the same as the rightmost panel in a 1x2 grid.
# ax21 = fig2.add_subplot(211) # add axes to the extra figures
# ax21 = fig2.add_subplot(212) # add axes to the extra figures
# ax31 = fig3.add_subplot(111) # add axes to the extra figures
plt.show()
Just use plt.subplots (different from plt.subplot) to define all your axes, with the option sharex=True:
f, axes = plt.subplots(2,2, sharex=True)
plt.subplot(122)
plt.show()
Note that the second call with larger subplot array overlay the preceding one.
Example (could not display image due to reputation...)

Matplotlib: Adjust legend location/position

I'm creating a figure with multiple subplots. One of these subplots is giving me some trouble, as none of the axes corners or centers are free (or can be freed up) for placing the legend. What I'd like to do is to have the legend placed somewhere in between the 'upper left' and 'center left' locations, while keeping the padding between it and the y-axis equal to the legends in the other subplots (that are placed using one of the predefined legend location keywords).
I know I can specify a custom position by using loc=(x,y), but then I can't figure out how to get the padding between the legend and the y-axis to be equal to that used by the other legends. Would it be possible to somehow use the borderaxespad property of the first legend? Though I'm not succeeding at getting that to work.
Any suggestions would be most welcome!
Edit: Here is a (very simplified) illustration of the problem:
import matplotlib.pyplot as plt
fig, ax = plt.subplots(1, 2, sharex=False, sharey=False)
ax[0].axhline(y=1, label='one')
ax[0].axhline(y=2, label='two')
ax[0].set_ylim([0.8,3.2])
ax[0].legend(loc=2)
ax[1].axhline(y=1, label='one')
ax[1].axhline(y=2, label='two')
ax[1].axhline(y=3, label='three')
ax[1].set_ylim([0.8,3.2])
ax[1].legend(loc=2)
plt.show()
What I'd like is that the legend in the right plot is moved down somewhat so it no longer overlaps with the line.
As a last resort I could change the axis limits, but I would very much like to avoid that.
I saw the answer you posted and tried it out. The problem however is that it is also depended on the figure size.
Here's a new try:
import numpy
import matplotlib.pyplot as plt
x = numpy.linspace(0, 10, 10000)
y = numpy.cos(x) + 2.
x_value = .014 #Offset by eye
y_value = .55
fig, ax = plt.subplots(1, 2, sharex = False, sharey = False)
fig.set_size_inches(50,30)
ax[0].plot(x, y, label = "cos")
ax[0].set_ylim([0.8,3.2])
ax[0].legend(loc=2)
line1 ,= ax[1].plot(x,y)
ax[1].set_ylim([0.8,3.2])
axbox = ax[1].get_position()
fig.legend([line1], ["cos"], loc = (axbox.x0 + x_value, axbox.y0 + y_value))
plt.show()
So what I am now doing is basically getting the coordinates from the subplot. I then create the legend based on the dimensions of the entire figure. Hence, the figure size does not change anything to the legend positioning anymore.
With the values for x_value and y_value the legend can be positioned in the subplot. x_value has been eyeballed for a good correspondence with the "normal" legend. This value can be changed at your desire. y_value determines the height of the legend.
Good luck!
After spending way too much time on this, I've come up with the following satisfactory solution (the Transformations Tutorial definitely helped):
bapad = plt.rcParams['legend.borderaxespad']
fontsize = plt.rcParams['font.size']
axline = plt.rcParams['axes.linewidth'] #need this, otherwise the result will be off by a few pixels
pad_points = bapad*fontsize + axline #padding is defined in relative to font size
pad_inches = pad_points/72.0 #convert from points to inches
pad_pixels = pad_inches*fig.dpi #convert from inches to pixels using the figure's dpi
Then, I found that both of the following work and give the same value for the padding:
# Define inverse transform, transforms display coordinates (pixels) to axes coordinates
inv = ax[1].transAxes.inverted()
# Inverse transform two points on the display and find the relative distance
pad_axes = inv.transform((pad_pixels, 0)) - inv.transform((0,0))
pad_xaxis = pad_axes[0]
or
# Find how may pixels there are on the x-axis
x_pixels = ax[1].transAxes.transform((1,0)) - ax[1].transAxes.transform((0,0))
# Compute the ratio between the pixel offset and the total amount of pixels
pad_xaxis = pad_pixels/x_pixels[0]
And then set the legend with:
ax[1].legend(loc=(pad_xaxis,0.6))
Plot:

Space between Y-axis and First X tick

Matplotlib newbie here.
I have the following code:
from pylab import figure, show
import numpy
fig = figure()
ax = fig.add_subplot(111)
plot_data=[1.7,1.7,1.7,1.54,1.52]
xdata = range(len(plot_data))
labels = ["2009-June","2009-Dec","2010-June","2010-Dec","2011-June"]
ax.plot(xdata,plot_data,"b-")
ax.set_xticks(range(len(labels)))
ax.set_xticklabels(labels)
ax.set_yticks([1.4,1.6,1.8])
fig.canvas.draw()
show()
When you run that code, the resulting chart has a run-in with the first tick label (2009-June) and the origin. How can I get the graph to move over to make that more readable? I tried to put dummy data in, but then Matplotlib (correctly) treats that as data.
add two limits to the x and y axes to shift the tick labels a bit.
# grow the y axis down by 0.05
ax.set_ylim(1.35, 1.8)
# expand the x axis by 0.5 at two ends
ax.set_xlim(-0.5, len(labels)-0.5)
the result is
Because tick labels are text objects you can change their alignment. However to get access to the text properties you need to go through the set_yticklabels function. So add the line:
ax.set_yticklabels([1.4,1.6,1.8],va="bottom")
after your set_yticks call. Alternatively if you go through the pylab library directly, instead of accessing the function through the axes object, you can just set that in one line:
pylab.yticks([1.4,1.6,1.8],va="bottom")
I suggest change Y axis limits:
ax.set_ylim([1.2, 1.8])

Categories

Resources