In matplotlib, how do I move the y tick labels to the far left side of the charting areas when I have moved the left spine?
Code:
import matplotlib.pyplot as plt
X = range(-10,5)
y = [i**2 for i in X]
fig, ax = plt.subplots(1,1, figsize=(10,8))
plt.plot(X, y)
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)
ax.spines['left'].set_position('zero')
Outputs:
However, I would like to get this:
Changing the transformation of the y-ticklabels back to the original y-axis transform would give you the desired ticks on the left side of the axes.
plt.setp(ax.get_yticklabels(), transform=ax.get_yaxis_transform())
Complete code for reproducibility
import matplotlib.pyplot as plt
X = range(-10,5)
y = [i**2 for i in X]
fig, ax = plt.subplots(1,1, figsize=(10,8))
plt.plot(X, y)
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)
ax.spines['left'].set_position('zero')
plt.setp(ax.get_yticklabels(), transform=ax.get_yaxis_transform())
plt.show()
You can exploit the set_position attribute of your tick labels by iterating over them. Here you have to pass the (xy) value to be set as the new location of your tickl-labels. We know the x-position is -10. What I found is that it does not matter what y-position you specify, the ticks are offset by the value of x which kind of makes sense too because the tick labels have to correspond to the ticks on the y-axis. However, one can't leave the y-value empty. So specifying (-10) won't work and (-10, 0), (-10, -100) or (-10, 1000) have no effect on the y-position of the ticks. The reason is as
#ImportanceOfBeingErnest clarified: The y position of the label will be determined by the y-values of your data and is set at draw time after applying the changes.
for tick in ax.yaxis.get_major_ticks():
print (tick.label.set_position((-10, 0)))
Related
I am facing a surprising behaviour regarding tick locators and twin axes.
I have an Axes object, and set a matplotlib.dates.DayLocator as its major locator, and a matplotlib.dates.DateFormatter as its major formatter.
import datetime as dt
import numpy as np
from matplotlib import figure as mpfig
from matplotlib.backends import backend_agg as mpback
from matplotlib import dates as mpdates
start = dt.datetime(2018, 2, 16)
end = start + dt.timedelta(days=10)
fig = mpfig.Figure(figsize=(16, 4))
mpback.FigureCanvas(fig)
ax = fig.add_subplot(111)
ax.xaxis.set_major_locator(mpdates.DayLocator())
ax.xaxis.set_major_formatter(mpdates.DateFormatter("%d/%m %H:%M"))
ax.grid(True)
x = np.arange(start, end, dt.timedelta(days=1)).astype(dt.datetime)
y = np.random.randint(10, 1000, size=x.size)
ax.plot(x, y)
fig.savefig('test.png')
Then, I create a twin Axes of it, with ax.twinx()
But as soon as I plot something on the new Axes, the tick format becomes the default one.
# ...
x = np.arange(start, end, dt.timedelta(days=1)).astype(dt.datetime)
y = np.random.randint(10, 1000, size=x.size)
ax2 = ax.twinx()
ax2.plot(x, y)
As you can see, the format is not the "%d/%m %H:%M" I specified anymore, but has become "%y-%m-%d".
Even more disturbing, if I set the x window and then plot something on ax2, my tick locator is ignored, and a default one is used.
# ...
ax.set_xlim(start, start + dt.timedelta(days=1))
ax.plot(x, y)
This code yield this figure:
# ...
ax.set_xlim(start, start + dt.timedelta(days=1))
ax2 = ax.twinx()
ax2.plot(x, y)
But this creates that:
Major ticks appeared at (I assume) default locations.
According to , the new Axes and the original one should share their limits, ticks and scale on the X axis:
Doc about Figure.add_axes, called by Axes.twinx
sharex, sharey : Axes, optional
Share the x or y axis with sharex and/or sharey. The axis will have the same limits, ticks, and scale as the axis of the shared axes.
So, what am I missing here?
Why does plotting on ax2 has any impact on ax's ticks, which I manually set?
The end result I'm attempting to achieve is to have a "thicker" black boarder around my plot, along xmin, xmax, ymin, & ymax. I've tried a couple of different things (such as just drawing a rectangle on the plot, see below), but I have not been able to achieve the desired results for a few reasons.
Because I cannot just use the spines (I've set 2 of them to always be at 0), I need to add some other line or rectangle to create the desired border.
By default the first and last tick labels overhang the axes. I "overcame" this by changing the horizontal or vertical alignment, but they could still use some more padding. I know this is possible, but requires a transform and is a bit clunky.
Now I'd like to remove the first and last tick marks on both axis. This is because given the way the rectangle is drawn it is always inside the plot area, but the first and last tick mark are always outside it, regardless of how thick the rectangle is. Making the rectangle thicker only causes it to overlap the first and last tick label more, which the actual tick mark remains outside the rectangle.
Any other suggestions on how to achieve this kind of border while always maintaining an axis at 0, 0 would be welcomed. That is the overall desired result.
import numpy as np
import matplotlib.pyplot as plt
import matplotlib
from matplotlib.patches import Rectangle
X = np.random.randint(low=-9, high=9, size=10)
Y = np.random.randint(low=-9, high=9, size=10)
fig, ax = plt.subplots()
ax.axis([-10, 10, -10, 10])
ax.spines['left'].set_position('zero')
ax.spines['bottom'].set_position('zero')
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)
plt.setp(ax.xaxis.get_majorticklabels()[0], ha='left')
plt.setp(ax.xaxis.get_majorticklabels()[-1], ha='right')
plt.setp(ax.yaxis.get_majorticklabels()[0], va='bottom')
plt.setp(ax.yaxis.get_majorticklabels()[-1], va='top')
patPlotBorder = ax.add_artist(Rectangle((-10, -10), 20, 20, fill=False, color='k', linewidth=2))
ax.grid(True)
fig.set_tight_layout(True)
ax.scatter(X, Y, c="b", marker="o", s=40)
plt.show()
Without changing much of your code, you can set the clip_on to False, such that the complete rectangle is shown.
border = Rectangle((-10, -10), 20, 20, fill=False, color='k', linewidth=3, clip_on=False)
ax.add_artist(border)
Since the gridlines are shown above the axes content, you have some grey line within the rectangle border.
Alternatively, you can use two axes. One with all the content and the modified spine positions etc., and one where you just make the spines bold and remove all the rest.
import numpy as np
import matplotlib.pyplot as plt
X = np.random.randint(low=-9, high=9, size=10)
Y = np.random.randint(low=-9, high=9, size=10)
fig, ax = plt.subplots()
ax2 = fig.add_subplot(111)
ax2.patch.set_visible(False)
ax2.tick_params(left=False, bottom=False, labelleft=False, labelbottom=False)
for _, sp in ax2.spines.items():
sp.set_linewidth(3)
ax.axis([-10, 10, -10, 10])
ax.spines['left'].set_position('zero')
ax.spines['bottom'].set_position('zero')
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)
plt.setp(ax.xaxis.get_majorticklabels()[0], ha='left')
plt.setp(ax.xaxis.get_majorticklabels()[-1], ha='right')
plt.setp(ax.yaxis.get_majorticklabels()[0], va='bottom')
plt.setp(ax.yaxis.get_majorticklabels()[-1], va='top')
ax.grid(True)
fig.set_tight_layout(True)
ax.scatter(X, Y, c="b", marker="o", s=40)
plt.show()
You can access the individual grid lines by calling get_{x|y}gridlines(). Each grid line is an object of type Line2D, and you can change any of their properties, such as thickness, color, etc.
ax.get_xgridlines()[0].set_linewidth(5)
ax.get_xgridlines()[-1].set_linewidth(5)
ax.get_ygridlines()[0].set_linewidth(5)
ax.get_ygridlines()[-1].set_linewidth(5)
Check the x axis of the figure below. How can I move the labels a bit to the left so that they align with their respective ticks?
I'm rotating the labels using:
ax.set_xticks(xlabels_positions)
ax.set_xticklabels(xlabels, rotation=45)
But, as you can see, the rotation is centered on the middle of the text labels. Which makes it look like they are shifted to the right.
I've tried using this instead:
ax.set_xticklabels(xlabels, rotation=45, rotation_mode="anchor")
... but it doesn't do what I wished for. And "anchor" seems to be the only value allowed for the rotation_mode parameter.
You can set the horizontal alignment of ticklabels, see the example below. If you imagine a rectangular box around the rotated label, which side of the rectangle do you want to be aligned with the tickpoint?
Given your description, you want: ha='right'
n=5
x = np.arange(n)
y = np.sin(np.linspace(-3,3,n))
xlabels = ['Ticklabel %i' % i for i in range(n)]
fig, axs = plt.subplots(1,3, figsize=(12,3))
ha = ['right', 'center', 'left']
for n, ax in enumerate(axs):
ax.plot(x,y, 'o-')
ax.set_title(ha[n])
ax.set_xticks(x)
ax.set_xticklabels(xlabels, rotation=40, ha=ha[n])
ha='right' is not enough to visually align labels with ticks:
For rotation=45, use both ha='right' and rotation_mode='anchor'
For other angles, use a ScaledTranslation() instead
rotation_mode='anchor'
If the rotation angle is roughly 45°, combine ha='right' with rotation_mode='anchor':
ax.set_xticks(ticks)
ax.set_xticklabels(labels, rotation=45, ha='right', rotation_mode='anchor')
Or in matplotlib 3.5.0+, set ticks and labels at once:
ax.set_xticks(ticks, labels, rotation=45, ha='right', rotation_mode='anchor')
ScaledTranslation()
If the rotation angle is more extreme (e.g., 70°) or you just want more fine-grained control, anchoring won't work well. Instead, apply a linear transform:
ax.set_xticks(ticks)
ax.set_xticklabels(labels, rotation=70)
# create -5pt offset in x direction
from matplotlib.transforms import ScaledTranslation
dx, dy = -5, 0
offset = ScaledTranslation(dx / fig.dpi, dy / fig.dpi, fig.dpi_scale_trans)
# apply offset to all xticklabels
for label in ax.xaxis.get_majorticklabels():
label.set_transform(label.get_transform() + offset)
Rotating the labels is certainly possible. Note though that doing so reduces the readability of the text. One alternative is to alternate label positions using a code like this:
import numpy as np
n=5
x = np.arange(n)
y = np.sin(np.linspace(-3,3,n))
xlabels = ['Long ticklabel %i' % i for i in range(n)]
fig, ax = plt.subplots()
ax.plot(x,y, 'o-')
ax.set_xticks(x)
labels = ax.set_xticklabels(xlabels)
for i, label in enumerate(labels):
label.set_y(label.get_position()[1] - (i % 2) * 0.075)
For more background and alternatives, see this post on my blog
An easy, loop-free alternative is to use the horizontalalignment Text property as a keyword argument to xticks[1]. In the below, at the commented line, I've forced the xticks alignment to be "right".
n=5
x = np.arange(n)
y = np.sin(np.linspace(-3,3,n))
xlabels = ['Long ticklabel %i' % i for i in range(n)]
fig, ax = plt.subplots()
ax.plot(x,y, 'o-')
plt.xticks(
[0,1,2,3,4],
["this label extends way past the figure's left boundary",
"bad motorfinger", "green", "in the age of octopus diplomacy", "x"],
rotation=45,
horizontalalignment="right") # here
plt.show()
(yticks already aligns the right edge with the tick by default, but for xticks the default appears to be "center".)
[1] You find that described in the xticks documentation if you search for the phrase "Text properties".
I am clearly late but there is an official example which uses
plt.setp(ax.get_xticklabels(), rotation=45, ha="right", rotation_mode="anchor")
to rotate the labels while keeping them correctly aligned with the ticks, which is both clean and easy.
See: https://matplotlib.org/stable/gallery/images_contours_and_fields/image_annotated_heatmap.html
I'm trying to create a plot with two Y axes (left and right) for the same data, that is, one is a scaled version of the other. I would like also to preserve the tick positions and grid positions, so the grid will match the ticks at both sides.
I'm trying to do this by plotting twice the same data, one as-is and the other scaled, but they are not coincident.
import numpy as np
import matplotlib.pyplot as plt
x = np.arange(17, 27, 0.1)
y1 = 0.05 * x + 100
fig, ax1 = plt.subplots()
ax2 = ax1.twinx()
ax1.plot(x, y1, 'g-')
ax2.plot(x, y1/max(y1), 'g-')
ax1.set_xlabel('X data')
ax1.set_ylabel('Y data', color='g')
ax2.set_ylabel('Y data normalized', color='b')
plt.grid()
plt.show()
Any help will be appreciated.
Not sure if you can achieve this without getting ugly-looking numbers on your normalized axis. But if that doesn't bother you, try adding this to your code:
ax2.set_ylim([ax1.get_ylim()[0]/max(y1),ax1.get_ylim()[1]/max(y1)])
ax2.set_yticks(ax1.get_yticks()/max(y1))
Probably not the most elegant solution, but it scales your axis limits and tick positions similarly to what you do with the data itself so the grid matches both axes.
In my plot, a secondary x axis is used to display the value of another variable for some data. Now, the original axis is log scaled. Unfortunaltely, the twinned axis puts the ticks (and the labels) referring to the linear scale of the original axis and not as intended to the log scale. How can this be overcome?
Here the code example that should put the ticks of the twinned axis in the same (absolute axes) position as the ones for the original axis:
def conv(x):
"""some conversion function"""
# ...
return x2
ax = plt.subplot(1,1,1)
ax.set_xscale('log')
# get the location of the ticks of ax
axlocs,axlabels = plt.xticks()
# twin axis and set limits as in ax
ax2 = ax.twiny()
ax2.set_xlim(ax.get_xlim())
#Set the ticks, should be set referring to the log scale of ax, but are set referring to the linear scale
ax2.set_xticks(axlocs)
# put the converted labels
ax2.set_xticklabels(map(conv,axlocs))
An alternative way would be (the ticks are then not set in the same position, but that doesn't matter):
from matplotlib.ticker import FuncFormatter
ax = plt.subplot(1,1,1)
ax.set_xscale('log')
ax2 = ax.twiny()
ax2.set_xlim(ax.get_xlim())
ax2.xaxis.set_major_formatter(FuncFormatter(lambda x,pos:conv(x)))
Both approaches work well as long as no log scale is used.
Perhaps there exists an easy fix. Is there something I missed in the documentation?
As a workaround, I tried to obtain the ax.transAxes coordinates of the ticks of ax and put the ticks at the very same position in ax2. But there does not exist something like
ax2.set_xticks(axlocs,transform=ax2.transAxes)
TypeError: set_xticks() got an unexpected keyword argument 'transform'
This has been asked a while ago, but I stumbled over it with the same question.
I eventually managed to solve the problem by introducing a logscaled (semilogx) transparent (alpha=0) dummy plot.
Example:
import numpy as np
import matplotlib.pyplot as plt
def conversion_func(x): # some arbitrary transformation function
return 2 * x**0.5 # from x to z
x = np.logspace(0, 5, 100)
y = np.sin(np.log(x))
fig = plt.figure()
ax = plt.gca()
ax.semilogx(x, y, 'k')
ax.set_xlim(x[0], x[-1]) # this is important in order that limits of both axes match
ax.set_ylabel("$y$")
ax.set_xlabel("$x$", color='C0')
ax.tick_params(axis='x', which='both', colors='C0')
ax.axvline(100, c='C0', lw=3)
ticks_x = np.logspace(0, 5, 5 + 1) # must span limits of first axis with clever spacing
ticks_z = conversion_func(ticks_x)
ax2 = ax.twiny() # get the twin axis
ax2.semilogx(ticks_z, np.ones_like(ticks_z), alpha=0) # transparent dummy plot
ax2.set_xlim(ticks_z[0], ticks_z[-1])
ax2.set_xlabel("$z \equiv f(x)$", color='C1')
ax2.xaxis.label.set_color('C1')
ax2.tick_params(axis='x', which='both', colors='C1')
ax2.axvline(20, ls='--', c='C1', lw=3) # z=20 indeed matches x=100 as desired
fig.show()
In the above example the vertical lines demonstrate that first and second axis are indeed shifted to one another as wanted. x = 100 gets shifted to z = 2*x**0.5 = 20. The colours are just to clarify which vertical line goes with which axis.
Don't need to cover them, just Eliminate the ticks!
d= [7,9,14,17,35,70];
j= [100,80,50,40,20,10];
plt.figure()
plt.xscale('log')
plt.plot(freq, freq*spec) #plot some spectrum
ax1 = plt.gca() #define my first axis
ax1.yaxis.set_ticks_position('both')
ax1.tick_params(axis='y',which='both',direction='in');
ax1.tick_params(axis='x',which='both',direction='in');
ax2 = ax1.twiny() #generates second axis (top)
ax2.set_xlim(ax1.get_xlim()); #same limits
plt.xscale('log') #make it log
ax2.set_xticks(freq[d]); #my own 'major' ticks OVERLAPS!!!
ax2.set_xticklabels(j); #change labels
ax2.tick_params(axis='x',which='major',direction='in');
ax2.tick_params(axis='x',which='minor',top=False); #REMOVE 'MINOR' TICKS
ax2.grid()
I think you can fix your issue by calling ax2.set_xscale('log').
import matplotlib.pyplot as plt
import numpy as np
fig, ax = plt.subplots()
ax.semilogx(np.logspace(1.0, 5.0, 20), np.random.random([20]))
new_tick_locations = np.array([10., 100., 1000., 1.0e4])
def tick_function(X):
V = X / 1000.
return ["%.3f" % z for z in V]
ax2 = ax.twiny()
ax2.set_xscale('log')
ax2.set_xlim(ax.get_xlim())
ax2.set_xticks(new_tick_locations)
ax2.set_xticklabels(tick_function(new_tick_locations))
ax2.set_xlabel(r"Modified x-axis: $X/1000$")