Pyplot - add single text to xaxis (like a tick) - python

Imagine you have a pyplot with a vertical line (matplotlib.axes.Axes.axvline) at a specific location x. Now I would like to have a text like "COG" on the x-axis at x, like as if it was a tick. It can be either on the visible or non-visible axis or both.
However,
ticks already exist (given array)
shared x-axis for subplots, only lowest visible
I though about using normal text (matplotlib.pyplot.text), but
it would be inside the subplot
it would not be in xaxis relation (at least I didn't find a working way so far)
I feel like manually editing the ticks to add a single item is a not so nice workaround..
Thanks ahead!

Here is an example graph of subplots from matplotlib:
import matplotlib.pyplot as plt
import numpy as np
# Some example data to display
x = np.linspace(0, 2 * np.pi, 400)
y = np.sin(x ** 2)
fig, axs = plt.subplots(2, sharex=True)
fig.suptitle('Vertically stacked subplots')
axs[0].plot(x, y)
axs[1].plot(x, -y)
To add the elements you wanted, you can just use axvline and text; Text elements can be outside of the boundaries of the graph (and in fact the tick labels are Text).
#continued from above:
axs[0].xaxis.set_visible(False)
axs[0].axvline(4.5, color='red')
axs[0].text(4.5, -.05, 'COG', color='red', transform=axs[0].get_xaxis_transform(),
ha='center', va='top')
axs[1].axvline(4.5, color='red')
axs[1].text(4.5, -.05, 'COG', color='red', transform=axs[1].get_xaxis_transform(),
ha='center', va='top')
You can instead add another tick and change its color:
#again, continued from the first code block
axs[0].xaxis.set_visible(False)
axs[0].axvline(4.5, color='red')
axs[0].text(4.5, -.05, 'COG', color='red', transform=axs[0].get_xaxis_transform(),
ha='center', va='top')
ticks = [0, 1, 2, 3, 4, 4.5, 5, 6]
labels = [0, 1, 2, 3, 4, "COG", 5, 6]
axs[1].axvline(4.5, color='red')
axs[1].set_xticks(ticks)
axs[1].set_xticklabels(labels)
axs[1].get_xticklabels()[5].set_color('red')
But, if you don't want ticks on the top graph, then it seems like adding Text (as in the first example) is simplest. Additionally, manually setting the ticks in the second example seems more verbose, and there's the issue of selecting the tick you want to change (I index with axs[1].get_xticklabels()[5] here, but with more ticks/nastier numbers you might need something smarter). So I prefer the first approach better than this, but it might be useful in some cases (like if you want your line to occur on an existing tick).

Using TomsĀ“s first example leads to the desired outcome.
Additionally, for the case of an overlapping text on the label, I searched for neighboring tick labels and set their transparency != 1. Thus, the text "cog" is always visible.
import matplotlib.pyplot as plt
import numpy as np
xV = 4.5
dxV = 1/4 # best 1/4 of the spacing between two ticks
# Some example data to display
x = np.linspace(0, 2 * np.pi, 400)
y = np.sin(x ** 2)
fig, axs = plt.subplots(2, sharex=True)
fig.suptitle('Vertically stacked subplots')
axs[0].plot(x, y)
axs[0].xaxis.set_visible(False)
axs[0].axvline(xV, color='red')
axs[0].text(xV, -.05, 'COG', color='red', transform=axs[0].get_xaxis_transform(),
ha='center', va='top')
axs[1].plot(x, -y)
axs[1].axvline(xV, color='red')
axs[1].text(xV, -.05, 'COG', color='red', transform=axs[1].get_xaxis_transform(),
ha='center', va='top')
# Change Transparency if too close
xticks = axs[1].xaxis.get_major_ticks()
values = axs[1].get_xticks()
# values = axs[1].xaxis.get_major_locator()()
pos = np.where(np.logical_and( (xV-dxV) <= values, (xV+dxV) >= values))[0]
if pos.size > 0:
dist = np.abs(values[pos]-xV)
pos = pos[dist.argmin()]
xticks[pos].label1.set_alpha(0.5)
plt.show()

Related

How can I add text to the same position in multiple matplotlib plots with different axis scales?

I have ~20 plots with different axes, ranging from scales of 0-1 to 0-300. I want to use plt.text(x,y) to add text to the top left corner in my automated plotting function, but the changing axis size does not allow for this to be automated and completely consistent.
Here are two example plots:
import matplotlib.pyplot as plt
plt.plot([1, 2, 3, 4])
plt.ylabel('some numbers')
plt.show()
#Plot 2
plt.plot([2, 4, 6, 8])
plt.ylabel('some numbers')
plt.show()
I want to use something like plt.text(x, y, 'text', fontsize=8) in both plots, but without specifying the x and y for each plot by hand, instead just saying that the text should go in the top left. Is this possible?
Have you tried ax.text with transform=ax.transAxes?
import matplotlib.pyplot as plt
fig, ax = plt.subplots()
ax.plot([1, 2, 3, 4])
ax.set_ylabel('XXX')
ax.text(0.05, 0.95, 'text', transform=ax.transAxes, fontsize=8, va='top', ha='left')
plt.show()
Explanation:
The ax.text takes first the x and y coordinates of the text, then the transform argument which specifies the coordinate system to use, and the va and ha arguments which specify the vertical and horizontal alignment of the text.
Use ax.annotate with axes fraction coordinates:
fig, axs = plt.subplots(1, 2)
axs[0].plot([0, 0.8, 1, 0.5])
axs[1].plot([10, 300, 200])
for ax in axs:
ax.annotate('text', (0.05, 0.9), xycoords='axes fraction')
# ------------------------
Here (0.05, 0.9) refers to 5% and 90% of the axes lengths, regardless of the data:

Is there a way to automatically fit the legend inside the ax of a figure with multiple subplots?

I am trying to show legends for each subplot in the figure that fit properly in the ax size (autoscaling fontsize, boxsize, etc.). I have tried many ways to fit legends but I have not found a non-intrincate manner. For example, in the answers to another posts (e.g. How to change legend size with matplotlib.pyplot) the user have to decide the legend size (fontsize and titlesize) but I am asking for a way to automatically fit the legend inside axes in a figure with multiple subplots.
I want the legend size to fit inside each ax in the Figure 1 created with the example code attached with ax.legend() line commented. In Figure 2 I show the figure obtained with ax.legend() line active : legends do not fit inside axes. Attached code is a modified version of the example in Matplotlib Examples : Creating multiple subplots using plt.subplots
import matplotlib.pyplot as plt
import numpy as np
# Some example data to display
x = np.linspace(0, 2 * np.pi, 400)
a = (x ** 2)
b = (x ** 3)
c = (x ** 4)
functions = [a,b,c]
label = "This is a long label for a legend and I try to fit to its ax"
fig, axs = plt.subplots(2, 2)
for y in functions :
axs[0, 0].plot(x, y, label=label)
axs[0, 1].plot(x, -y, label=label)
axs[1, 0].plot(x, y, label=label)
axs[1, 1].plot(x, -y, label=label)
for ax in axs.flat:
ax.set_title('Title')
ax.set(xlabel='x-label', ylabel='y-label')
ax.legend() # commented for the Figure 1 and active for the Figure 2
fig.tight_layout()
Thank you in advance.
Changing the size of the figure away from the default Matplotlib figure sizes should be able to help with your problem (I personally like to use plt.rcParams but just know that this changes all figure sizes downstream). At a certain point, if your legend is still too big even with a big figure, then you should consider editing the length of your legend. The point of graphs is to give the reader a snapshot understanding of your data. If they have to read a paragraph just in the legend, then the graph is not structured correctly.
import matplotlib.pyplot as plt
import numpy as np
# Some example data to display
x = np.linspace(0, 2 * np.pi, 400)
a = (x ** 2)
b = (x ** 3)
c = (x ** 4)
functions = [a,b,c]
label = "This is a long label for a legend and I try to fit to its ax"
plt.rcParams["figure.figsize"] = (20,10)
fig, axs = plt.subplots(2, 2)
for y in functions :
axs[0, 0].plot(x, y, label=label)
axs[0, 1].plot(x, -y, label=label)
axs[1, 0].plot(x, y, label=label)
axs[1, 1].plot(x, -y, label=label)
for ax in axs.flat:
ax.set_title('Title')
ax.set(xlabel='x-label', ylabel='y-label')
ax.legend() # commented for the Figure 1 and active for the Figure 2
fig.tight_layout()
Output graph:
~~ Some more control ~~
To not change the figure size at all but only change the legend size based on the length of the legend (i.e. Dynamically) try something like this. It defines the max size that the legend will get (to stop short legend texts from creating huge legends), finds the length of the text in the legend, finds what the legend Size should be (but not larger than you set max legend size), and sets the legend size accordingly.
import matplotlib.pyplot as plt
import numpy as np
# Some example data to display
x = np.linspace(0, 2 * np.pi, 400)
a = (x ** 2)
b = (x ** 3)
c = (x ** 4)
functions = [a,b,c]
label1 = "This is a long label for a legend and I try to fit to its ax"
label2 = "Short label"
label3 = "This is a long label for a legend and I try to fit to its ax, even longer for no reason basically a paragraph look at this thing keep going"
label4 = "This is a long label for a legend"
labels=[label1, label2, label3, label4]
fig, axs = plt.subplots(2, 2)
for y in functions :
axs[0, 0].plot(x, y, label=label1)
axs[0, 1].plot(x, -y, label=label2)
axs[1, 0].plot(x, y, label=label3)
axs[1, 1].plot(x, -y, label=label4)
maxLegendSize = 2
for ax, label in zip(axs.flat, labels):
ax.set_title('Title')
ax.set(xlabel='x-label', ylabel='y-label')
legendSize = maxLegendSize * (100/len(label))
if legendSize > maxLegendSize:
legendSize = maxLegendSize
ax.legend(prop={'size': legendSize})
fig.tight_layout()
plt.show()
Default figure size (with maxLegendSize = 2):
With a changed figure size and thus a changed maxLegendSize = 10:

How to have a common y-label between two subplots? [duplicate]

I have the following plot:
fig,ax = plt.subplots(5,2,sharex=True,sharey=True,figsize=fig_size)
and now I would like to give this plot common x-axis labels and y-axis labels. With "common", I mean that there should be one big x-axis label below the whole grid of subplots, and one big y-axis label to the right. I can't find anything about this in the documentation for plt.subplots, and my googlings suggest that I need to make a big plt.subplot(111) to start with - but how do I then put my 5*2 subplots into that using plt.subplots?
This looks like what you actually want. It applies the same approach of this answer to your specific case:
import matplotlib.pyplot as plt
fig, ax = plt.subplots(nrows=3, ncols=3, sharex=True, sharey=True, figsize=(6, 6))
fig.text(0.5, 0.04, 'common X', ha='center')
fig.text(0.04, 0.5, 'common Y', va='center', rotation='vertical')
Since I consider it relevant and elegant enough (no need to specify coordinates to place text), I copy (with a slight adaptation) an answer to another related question.
import matplotlib.pyplot as plt
fig, axes = plt.subplots(5, 2, sharex=True, sharey=True, figsize=(6,15))
# add a big axis, hide frame
fig.add_subplot(111, frameon=False)
# hide tick and tick label of the big axis
plt.tick_params(labelcolor='none', which='both', top=False, bottom=False, left=False, right=False)
plt.xlabel("common X")
plt.ylabel("common Y")
This results in the following (with matplotlib version 2.2.0):
New in Matplotlib v3.4 (pip install matplotlib --upgrade)
supxlabel and supylabel
fig.supxlabel('common_x')
fig.supylabel('common_y')
See example:
import matplotlib.pyplot as plt
for tl, cl in zip([True, False, False], [False, False, True]):
fig = plt.figure(constrained_layout=cl, tight_layout=tl)
gs = fig.add_gridspec(2, 3)
ax = dict()
ax['A'] = fig.add_subplot(gs[0, 0:2])
ax['B'] = fig.add_subplot(gs[1, 0:2])
ax['C'] = fig.add_subplot(gs[:, 2])
ax['C'].set_xlabel('Booger')
ax['B'].set_xlabel('Booger')
ax['A'].set_ylabel('Booger Y')
fig.suptitle(f'TEST: tight_layout={tl} constrained_layout={cl}')
fig.supxlabel('XLAgg')
fig.supylabel('YLAgg')
plt.show()
see more
Without sharex=True, sharey=True you get:
With it you should get it nicer:
fig, axes2d = plt.subplots(nrows=3, ncols=3,
sharex=True, sharey=True,
figsize=(6,6))
for i, row in enumerate(axes2d):
for j, cell in enumerate(row):
cell.imshow(np.random.rand(32,32))
plt.tight_layout()
But if you want to add additional labels, you should add them only to the edge plots:
fig, axes2d = plt.subplots(nrows=3, ncols=3,
sharex=True, sharey=True,
figsize=(6,6))
for i, row in enumerate(axes2d):
for j, cell in enumerate(row):
cell.imshow(np.random.rand(32,32))
if i == len(axes2d) - 1:
cell.set_xlabel("noise column: {0:d}".format(j + 1))
if j == 0:
cell.set_ylabel("noise row: {0:d}".format(i + 1))
plt.tight_layout()
Adding label for each plot would spoil it (maybe there is a way to automatically detect repeated labels, but I am not aware of one).
Since the command:
fig,ax = plt.subplots(5,2,sharex=True,sharey=True,figsize=fig_size)
you used returns a tuple consisting of the figure and a list of the axes instances, it is already sufficient to do something like (mind that I've changed fig,axto fig,axes):
fig,axes = plt.subplots(5,2,sharex=True,sharey=True,figsize=fig_size)
for ax in axes:
ax.set_xlabel('Common x-label')
ax.set_ylabel('Common y-label')
If you happen to want to change some details on a specific subplot, you can access it via axes[i] where i iterates over your subplots.
It might also be very helpful to include a
fig.tight_layout()
at the end of the file, before the plt.show(), in order to avoid overlapping labels.
It will look better if you reserve space for the common labels by making invisible labels for the subplot in the bottom left corner. It is also good to pass in the fontsize from rcParams. This way, the common labels will change size with your rc setup, and the axes will also be adjusted to leave space for the common labels.
fig_size = [8, 6]
fig, ax = plt.subplots(5, 2, sharex=True, sharey=True, figsize=fig_size)
# Reserve space for axis labels
ax[-1, 0].set_xlabel('.', color=(0, 0, 0, 0))
ax[-1, 0].set_ylabel('.', color=(0, 0, 0, 0))
# Make common axis labels
fig.text(0.5, 0.04, 'common X', va='center', ha='center', fontsize=rcParams['axes.labelsize'])
fig.text(0.04, 0.5, 'common Y', va='center', ha='center', rotation='vertical', fontsize=rcParams['axes.labelsize'])
Update:
This feature is now part of the proplot matplotlib package that I recently released on pypi. By default, when you make figures, the labels are "shared" between subplots.
Original answer:
I discovered a more robust method:
If you know the bottom and top kwargs that went into a GridSpec initialization, or you otherwise know the edges positions of your axes in Figure coordinates, you can also specify the ylabel position in Figure coordinates with some fancy "transform" magic.
For example:
import matplotlib.pyplot as plt
import matplotlib.transforms as mtransforms
bottom, top = 0.1, 0.9
fig, axs = plt.subplots(nrows=2, ncols=1, bottom=bottom, top=top)
avepos = 0.5 * (bottom + top)
transform = mtransforms.blended_transform_factory(mtransforms.IdentityTransform(), fig.transFigure) # specify x, y transform
axs[0].yaxis.label.set_transform(transform) # changed from default blend (IdentityTransform(), axs[0].transAxes)
axs[0].yaxis.label.set_position((0, avepos))
axs[0].set_ylabel('Hello, world!')
...and you should see that the label still appropriately adjusts left-right to keep from overlapping with labels, just like normal, but will also position itself exactly between the desired subplots.
Notably, if you omit the set_position call, the ylabel will show up exactly halfway up the figure. I'm guessing this is because when the label is finally drawn, matplotlib uses 0.5 for the y-coordinate without checking whether the underlying coordinate transform has changed.
I ran into a similar problem while plotting a grid of graphs. The graphs consisted of two parts (top and bottom). The y-label was supposed to be centered over both parts.
I did not want to use a solution that depends on knowing the position in the outer figure (like fig.text()), so I manipulated the y-position of the set_ylabel() function. It is usually 0.5, the middle of the plot it is added to. As the padding between the parts (hspace) in my code was zero, I could calculate the middle of the two parts relative to the upper part.
import matplotlib.pyplot as plt
import matplotlib.gridspec as gridspec
# Create outer and inner grid
outerGrid = gridspec.GridSpec(2, 3, width_ratios=[1,1,1], height_ratios=[1,1])
somePlot = gridspec.GridSpecFromSubplotSpec(2, 1,
subplot_spec=outerGrid[3], height_ratios=[1,3], hspace = 0)
# Add two partial plots
partA = plt.subplot(somePlot[0])
partB = plt.subplot(somePlot[1])
# No x-ticks for the upper plot
plt.setp(partA.get_xticklabels(), visible=False)
# The center is (height(top)-height(bottom))/(2*height(top))
# Simplified to 0.5 - height(bottom)/(2*height(top))
mid = 0.5-somePlot.get_height_ratios()[1]/(2.*somePlot.get_height_ratios()[0])
# Place the y-label
partA.set_ylabel('shared label', y = mid)
plt.show()
picture
Downsides:
The horizontal distance to the plot is based on the top part, the bottom ticks might extend into the label.
The formula does not take space between the parts into account.
Throws an exception when the height of the top part is 0.
There is probably a general solution that takes padding between figures into account.

Exclude grid lines from boundaries of plot in python?

I am looking for a way to remove grid lines from the axes of a plot, but unfortunately, I've not come up to a solution for this issue and neither found it anywhere else.
Is there a way to remove certain grid lines or choose which grid lines to plot without having to rely on the automatic function?
I've coded a quick example outputting a plot for illustration below and would be glad for any help.
import matplotlib.pyplot as plt
import numpy as np
def linear(x, a, b):
return a*x+b
x = np.linspace(0, 1, 20)
y = linear(x, a=1, b=2)
fig, ax = plt.subplots(nrows=1, ncols=1, figsize=(10, 6))
ax.plot(x, y, color='darkred')
ax.set_xlim(0, 1)
ax.set_ylim(2, 3)
ax.grid(which='major', axis='y', linestyle='--', color='grey', linewidth=3)
plt.savefig("Testplot.pdf", format='pdf')
The major gridlines appear at positions of the major ticks. You can set any individual gridline invisible. E.g. to set the fifth gridline off,
ax.yaxis.get_major_ticks()[5].gridline.set_visible(False)
Here is a proposition with ticks and horizontal lines. The idea is to specify the ticks (not really necessary, but why not), and then to draw horizontal dashes lines where you want your grid.
import matplotlib.pyplot as plt
import numpy as np
def linear(x, a, b):
return a*x+b
x = np.linspace(0, 1, 20)
y = linear(x, a=1, b=2)
fig, ax = plt.subplots(nrows=1, ncols=1, figsize=(10, 6))
ax.plot(x, y, color='darkred')
ax.set_xlim(0, 1)
ax.set_ylim(2, 3)
yticks = np.arange(2, 3, 0.2)
grid_lines = np.arange(2.2, 3, 0.2)
ax.set_yticks(yticks)
for grid in grid_lines:
ax.axhline(grid, linestyle='--', color='grey', linewidth=3)
Output:
Why did I include the yticks? Well you could design a function which takes in input the yticks and return the position of the grid lines accordingly. I think it could be handy depending on your needs. Good luck!

Showing legend for only one subplot using matplotlib

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.

Categories