Axis labels in line with tick labels in matplotlib - python

For space reasons, I sometimes make plots in the following style:
fig, ax = plt.subplots(figsize=(3, 3))
ax.plot([0,1], [1000, 1001])
ax.set_xticks([0, 1])
ax.set_yticks([1000, 1001])
ax.set_xlabel("x", labelpad=-8)
ax.set_ylabel("y", labelpad=-18)
Here, I've kept just ticks marking the boundaries of the X/Y domains, and I'm manually aligning the xlabel and ylabel using the labelpad keyword argument so that the x and y axis labels visually align with the tick labels.
Note, I've had to use different amounts of padding for the different axes, since the length of the y tick labels 1000 and 1001 extends farther away from the axis than the height of the x tick labels 0 and 1, and since the vertical position of the x axis label and the horizontal position of the y axis label are relative to their usual position, which would be just past the extent of the tick labels.
I'm wondering, is there a way to automate this procedure, and to do it exactly rather than visually? For example, if labelpad were relative to the spines, that would be very nice, or if there were a way to determine the extent of the ticks and tick labels away from the spines, that number could be used to automate this as well.
A similar effect can be obtained using ax.yaxis.set_label_coords, but this transforms the position relative to the axes' transform, and thus depends on the size of the axes, while the ticks are positioned absolutely relative to the spines.

The path you were going down with ax.{x,y}axis.set_label_coords was pretty much there! All you need to do is wrap the transAxes transform in an offset_copy and then provide an offset that is a combination of the current length of the ticks + any space around the tick bbox.
Using Transforms
import matplotlib.pyplot as plt
from matplotlib.transforms import offset_copy
fig, ax = plt.subplots(figsize=(3, 3))
fig.set_facecolor('white')
ax.plot([0,1], [1000, 1001])
ax.set_xticks([0, 1])
ax.set_yticks([1000, 1001])
# Create a transform that vertically offsets the label
# starting at the edge of the Axes and moving downwards
# according to the total length of the bounding box of a major tick
t = offset_copy(
ax.transAxes, y=-(ax.xaxis.get_tick_padding() + ax.xaxis.majorTicks[0].get_pad()),
fig=fig, units='dots'
)
ax.xaxis.set_label_coords(.5, 0, transform=t)
ax.set_xlabel('x', va='top')
# Repeat the above, but on the y-axis
t = offset_copy(
ax.transAxes, x=-(ax.yaxis.get_tick_padding() + ax.yaxis.majorTicks[0].get_pad()),
fig=fig, units='dots'
)
ax.yaxis.set_label_coords(0, .5, transform=t)
ax.set_ylabel('y', va='bottom')
Test with longer ticks
import matplotlib.pyplot as plt
from matplotlib.transforms import offset_copy
fig, ax = plt.subplots(figsize=(3, 3))
fig.set_facecolor('white')
ax.plot([0,1], [1000, 1001])
ax.set_xticks([0, 1])
ax.set_yticks([1000, 1001])
ax.xaxis.set_tick_params(length=10)
ax.yaxis.set_tick_params(length=15)
t = offset_copy(
ax.transAxes, y=-(ax.xaxis.get_tick_padding() + ax.xaxis.majorTicks[0].get_pad()),
fig=fig, units='points'
)
ax.xaxis.set_label_coords(.5, 0, transform=t)
ax.set_xlabel('x', va='top')
t = offset_copy(
ax.transAxes, x=-(ax.yaxis.get_tick_padding() + ax.yaxis.majorTicks[0].get_pad()),
fig=fig, units='points'
)
ax.yaxis.set_label_coords(0, .5, transform=t)
ax.set_ylabel('y', va='bottom')
Longer ticks & increased DPI
import matplotlib.pyplot as plt
from matplotlib.transforms import offset_copy
fig, ax = plt.subplots(figsize=(3, 3), dpi=150)
fig.set_facecolor('white')
ax.plot([0,1], [1000, 1001])
ax.set_xticks([0, 1])
ax.set_yticks([1000, 1001])
ax.xaxis.set_tick_params(length=10)
ax.yaxis.set_tick_params(length=15)
t = offset_copy(
ax.transAxes, y=-(ax.xaxis.get_tick_padding() + ax.xaxis.majorTicks[0].get_pad()),
fig=fig, units='points'
)
ax.xaxis.set_label_coords(.5, 0, transform=t)
ax.set_xlabel('x', va='top')
t = offset_copy(
ax.transAxes, x=-(ax.yaxis.get_tick_padding() + ax.yaxis.majorTicks[0].get_pad()),
fig=fig, units='points'
)
ax.yaxis.set_label_coords(0, .5, transform=t)
ax.set_ylabel("y", va='bottom')

Related

Matplotlib colorbar labels to fit into boxes

I created a scatter plot using matplotlib but I am somehow unable to get the labels to center into the boxes within the colorbar..
This is the code I have so far:
cMap = ListedColormap(['Orange', 'Purple', 'Blue','Red','Green'])
fig, ax = plt.subplots()
plt.figure(figsize=(12,12),dpi = 80)
#data
dist = np.random.rand(1900,1900)
#legend
cbar = plt.colorbar(scatter)
cbar.ax.get_yaxis().set_ticks([])
for j, lab in enumerate(['$Training$','$None$','$GS$','$ML$','$Both$']):
cbar.ax.text( .5, j - .985, lab, ha='left', va='center', rotation = 270)
cbar.ax.get_yaxis().labelpad = 15
cbar.ax.set_ylabel('Outliers', rotation=270)
indices = np.where(outlier_label != -2)[0]
plt.scatter(dist[indices, 0], dist[indices, 1], c=outlier_label[indices], cmap=cMap, s=20)
plt.gca().set_aspect('equal', 'datalim')
plt.title('Projection of the data', fontsize=24)
Thanks!
In line cbar.ax.text( .5, j - .985, lab, ha='left', va='center', rotation = 270) you have to work and change with '.985' with try and error to get better results.
You can extract the y limits of the colorbar to know its top and bottom. Dividing that area into 11 equally spaced positions, will have the 5 centers at the odd positions of that list. Similarly, you can extract the x limits to find the horizontal center.
Some remarks:
If you already called plt.subplots(), then plt.figure() will create a new figure, leaving the first plot empty. You can set the figsize directly via plt.subplots(figsize=...)
You are mixing matplotlib's "object-oriented interface" with the pyplot interface. This can lead to a lot of confusion. It is best to stick to one or the other. (The object-oriented interface is preferred, especially when you are creating non-trivial plots.)
You set dist = np.random.rand(1900,1900) of dimensions 1900x1900 while you are only using dimensions 1900x2.
The code nor the text give an indication of the values inside outlier_label. The code below assumes they are 5 equally-spaced numbers, and that both the lowest and the highest value are present in the data.
import matplotlib.pyplot as plt
from matplotlib.colors import ListedColormap
import numpy as np
colors = ['Orange', 'Purple', 'Blue', 'Red', 'Green']
cmap = ListedColormap(colors)
fig, ax = plt.subplots(figsize=(12, 12), dpi=80)
# data
dist = np.random.randn(1900, 2).cumsum(axis=0)
outlier_label = np.repeat(np.arange(5), 1900 // 5)
indices = outlier_label != -2
scatter = ax.scatter(dist[indices, 0], dist[indices, 1], c=outlier_label[indices], cmap=cmap, s=20)
# legend
cbar = plt.colorbar(scatter, ax=ax)
cbar.ax.get_yaxis().set_ticks([])
cb_xmin, cb_xmax = cbar.ax.get_xlim()
cb_ymin, cb_ymax = cbar.ax.get_ylim()
num_colors = len(colors)
for j, lab in zip(np.linspace(cb_ymin, cb_ymax, 2 * num_colors + 1)[1::2],
['$Training$', '$None$', '$GS$', '$ML$', '$Both$']):
cbar.ax.text((cb_xmin + cb_xmax) / 2, j, lab, ha='center', va='center', rotation=270, color='white', fontsize=16)
cbar.ax.get_yaxis().labelpad = 25
cbar.ax.set_ylabel('Outliers', rotation=270, fontsize=18)
ax.set_aspect('equal', 'datalim')
ax.set_title('Projection of the data', fontsize=24)
plt.show()

End ticks missing on colorbars in matplotlib

I want to make contour plots with colorbars that are symmetric about zero and have ticks at the maximum and minimum values. I am having a problem where the end ticks on my colorbars are not showing.
Here is an example:
fig, ax = plt.subplots()
A = np.random.random((10,10))*10-5
x = np.arange(0, A.shape[1])
y = np.arange(0, A.shape[0])
minval=-5
maxval=5
im1 = ax.contourf(x,y,A,150, vmin=minval, vmax=maxval,cmap="BrBG",extend='both')
cbar = ax.figure.colorbar(
im1,
ax=ax,
ticks=[minval, minval/2, 0, maxval/2, maxval],
orientation="vertical",
)
Which results in this figure (it will not let me embed the image, see link), that has tick marks at 0 and +/-2.5 but not +/-5:
contour plot with a colorbar that has ticks at -2.5, 0 and 2.5 but not at -5 or 5
I tried these following add-ons with no avail:
im1.set_clim(minval, maxval)
cbar.ax.set_xticklabels([minval, minval/2, '0', maxval/2, maxval])
plt.show()
I'm nearly positive that this used to work fine but has recently been skipping out on the end tick marks. I'm running it in a jupyter notebook. Ideas?
If you call contourf(..., levels=150, ...), matplotlib will create 151 equally spaced boundaries between the minimum and maximum of the data. The way np.random.random works, the minimum of A is slightly larger than -5 and the maximum slightly smaller than 5. So, these extreme values don't belong to the level boundaries. And won't be visible in the colorbar. To have them visible, the level boundaries could be set explicitly to include these values:
from matplotlib import pyplot as plt
import numpy as np
fig, ax = plt.subplots()
A = np.random.random((10, 10)) * 10 - 5
x = np.arange(0, A.shape[1])
y = np.arange(0, A.shape[0])
minval = -5
maxval = 5
im1 = ax.contourf(x, y, A, levels=np.linspace(minval, maxval, 151), cmap="BrBG", extend='both')
cbar = fig.colorbar(
im1,
ax=ax,
ticks=[minval, minval/2, 0, maxval/2, maxval],
orientation="vertical"
)
cbar.ax.set_yticklabels([minval, minval/2, "0", maxval/2, maxval])
plt.show()
Also note that for a vertical colorbar, the y-direction is used (cbar.ax.set_yticklabels).
Do you specifically need to be using contourf? If you use pcolormesh instead, almost your exact code will give you the ticks where you want (you just have to move "extend" to the cbar call vs the pcolormesh call). That doesn't answer your question, but dodges the issue...
fig, ax = plt.subplots()
A = np.random.random((10,10))*10-5
x = np.arange(0, A.shape[1])
y = np.arange(0, A.shape[0])
minval=-5
maxval=5
# im1 = ax.contourf(x,y,A,150, vmin=minval-10, vmax=maxval+10,cmap=plt.cm.get_cmap("BrBG",32),extend='both')
im1 = ax.pcolormesh(x,y,A,vmin=minval,vmax=maxval,cmap=plt.cm.get_cmap('BrBG'))
cbar = fig.colorbar(
im1,
ax=ax,
ticks=[minval, minval/2, 0, maxval/2, maxval],
orientation="vertical", extend='both'
)

How to add one custom tickline in matplotlib axes

Is there a programmatic way to force the appearance of a single tickline at the additional tick location shown below?
Requirements:
Tickline should be pointing down from x-axis
Tickline should extend to label 103 regardless of padding
Tickline should be the same color and thickness of axes
No changes to other ticks (ticklines or tick labels)
Code and sample image:
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.ticker as ticker
fig, ax = plt.subplots(figsize=(6, 8 / 3))
fig.tight_layout()
x_limit = 103
x = [0, 10, 50, 100, x_limit]
y = [6, 2, 5, 2, 20]
ax.plot(x, y)
# Add a tick which represents the maximum x-value
xticks = ax.xaxis.get_majorticklocs()
ax.xaxis.set_ticks(np.append(xticks, x_limit))
# Change padding of tick in the event other ticks get too close
tick = ax.get_xaxis().get_major_ticks()[-1]
tick.set_pad(14)
tick.label1 = tick._get_text1()
# Set tight axes bounds around data
ax.set_ylim(0, max(y) + 1)
ax.set_xlim(0, x_limit)
EDIT: Tried tcaswell's solution and ended up with an annotation in the right place. I do notice some aliasing as if it doesn't look like an extension of the y-axis. Any ideas on how to clean this up?
You can do this with annotate and a bit of transform magic.
import matplotlib.transforms as mtransforms
import matplotlib.pyplot as plt
fig, ax = plt.subplots(figsize=(9, 3), tight_layout=True)
ax.set_xlim(90, 105)
trans = mtransforms.blended_transform_factory(ax.transData, ax.transAxes)
tt = ax.annotate('103', (103, 0), xytext=(0, -12), transform=trans,
arrowprops={'arrowstyle': '-'}, ha='center', va='top',
textcoords='offset points')

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)

How to add a single colobar that will show the data from 2 different subplot

What i wanna do is adding a single colorbar (at the right side of the figure shown below), that will show the colorbar for both subplots (they are at the same scale).
Another thing doesn't really make sense for me is why the lines I try to draw on the end of the code are not drawn (they are supposed to be horizontal lines on the center of both plots)
Thanks for the help.
Here are the code:
idx=0
b=plt.psd(dOD[:,idx],Fs=self.fs,NFFT=512)
B=np.zeros((2*len(self.Chan),len(b[0])))
B[idx,:]=20*log10(b[0])
c=plt.psd(dOD_filt[:,idx],Fs=self.fs,NFFT=512)
C=np.zeros((2*len(self.Chan),len(b[0])))
C[idx,:]=20*log10(c[0])
for idx in range(2*len(self.Chan)):
b=plt.psd(dOD[:,idx],Fs=self.fs,NFFT=512)
B[idx,:]=20*log10(b[0])
c=plt.psd(dOD_filt[:,idx],Fs=self.fs,NFFT=512)
C[idx,:]=20*log10(c[0])
## Calculate the color scaling for the imshow()
aux1 = max(max(B[i,:]) for i in range(size(B,0)))
aux2 = min(min(B[i,:]) for i in range(size(B,0)))
bux1 = max(max(C[i,:]) for i in range(size(C,0)))
bux2 = min(min(C[i,:]) for i in range(size(C,0)))
scale1 = 0.75*max(aux1,bux1)
scale2 = 0.75*min(aux2,bux2)
fig, axes = plt.subplots(nrows=2, ncols=1,figsize=(7,7))#,sharey='True')
fig.subplots_adjust(wspace=0.24, hspace=0.35)
ii=find(c[1]>=frange)[0]
## Making the plots
cax=axes[0].imshow(B, origin = 'lower',vmin=scale2,vmax=scale1)
axes[0].set_ylim((0,2*len(self.Chan)))
axes[0].set_xlabel(' Frequency (Hz) ')
axes[0].set_ylabel(' Channel Number ')
axes[0].set_title('Pre-Filtered')
cax2=axes[1].imshow(C, origin = 'lower',vmin=scale2,vmax=scale1)
axes[1].set_ylim(0,2*len(self.Chan))
axes[1].set_xlabel(' Frequency (Hz) ')
axes[1].set_ylabel(' Channel Number ')
axes[1].set_title('Post-Filtered')
axes[0].annotate('690nm', xy=((ii+1)/2, len(self.Chan)/2-1),
xycoords='data', va='center', ha='right')
axes[0].annotate('830nm', xy=((ii+1)/2, len(self.Chan)*3/2-1 ),
xycoords='data', va='center', ha='right')
axes[1].annotate('690nm', xy=((ii+1)/2, len(self.Chan)/2-1),
xycoords='data', va='center', ha='right')
axes[1].annotate('830nm', xy=((ii+1)/2, len(self.Chan)*3/2-1 ),
xycoords='data', va='center', ha='right')
axes[0].axis('tight')
axes[1].axis('tight')
## Set up the xlim to aprox frange Hz
axes[0].set_xlim(left=0,right=ii)
axes[1].set_xlim(left=0,right=ii)
## Make the xlabels become the actual frequency number
ticks = linspace(0,ii,10)
tickslabel = linspace(0.,frange,10)
for i in range(10):
tickslabel[i]="%.1f" % tickslabel[i]
axes[0].set_xticks(ticks)
axes[0].set_xticklabels(tickslabel)
axes[1].set_xticks(ticks)
axes[1].set_xticklabels(tickslabel)
## Draw a line to separate the two different wave lengths, and name each region
l1 = Line2D([0,frange],[28,28],ls='-',color='black')
axes[0].add_line(l1)
axes[1].add_line(l1)
And here the figure it makes:
If any more info are needed, just ask.
Basically, figure.colorbar() is good for both images, as long as their are not with too different scales. So you could let matplotlib do it for you... or you manually position your colorbar on axes inside the images. Here is how to control the location of the colorbar:
import numpy as np
from matplotlib import pyplot as plt
A = np.random.random_integers(0, 10, 100).reshape(10, 10)
B = np.random.random_integers(0, 10, 100).reshape(10, 10)
fig = plt.figure()
ax1 = fig.add_subplot(221)
ax2 = fig.add_subplot(222)
mapable = ax1.imshow(A, interpolation="nearest")
cax = ax2.imshow(A, interpolation="nearest")
# set the tickmarks *if* you want cutom (ie, arbitrary) tick labels:
cbar = fig.colorbar(cax, ax=None)
fig = plt.figure(2)
ax1 = fig.add_subplot(121)
ax2 = fig.add_subplot(122)
mapable = ax1.imshow(A, interpolation="nearest")
cax = ax2.imshow(A, interpolation="nearest")
# on the figure total in precent l b w , height
ax3 = fig.add_axes([0.1, 0.1, 0.8, 0.05]) # setup colorbar axes.
# put the colorbar on new axes
cbar = fig.colorbar(mapable,cax=ax3,orientation='horizontal')
plt.show()
Note ofcourse you can position ax3 as you wish, on the side, on the top, where ever,
as long as it is in the boundaries of the figure.
I don't know why your line2D is not appearing.
I added to my code before plt.show() the following and everything is showing:
from mpl_toolkits.axes_grid1 import anchored_artists
from matplotlib.patheffects import withStroke
txt = anchored_artists.AnchoredText("SC",
loc=2,
frameon=False,
prop=dict(size=12))
if withStroke:
txt.txt._text.set_path_effects([withStroke(foreground="w",
linewidth=3)])
ax1.add_artist(txt)
## Draw a line to separate the two different wave lengths, and name each region
l1 = plt.Line2D([-1,10],[5,5],ls='-',color='black',lineswidth=10)
ax1.add_line(l1)

Categories

Resources