I have been trying to set up secondary 'y' and 'x' axes on the filled contour plot using matplotlib
. My goal is to have additional 'x' and 'y' axis such that both are function of the values of their primary axes. To make it work I have played around with secondary_yaxis
and secondary_xaxis
. I am confused how these functions work really. The actual functions and data are different but here is the code I was playing around with to understand what's happening:
import matplotlib.pyplot as plt
import numpy as np
# the function that I'm going to plot
def z_func(x, y):
return (1 - (x ** 2 + y ** 3)) * np.exp(-(x ** 2 + y ** 2) / 2)
yMax = 2.5
yMin = 1
xMax = yMax
xMin = yMin
x = np.arange(yMin, yMax, 0.01)
y = np.arange(xMin, xMax, 0.01)
X, Y = np.meshgrid(x, y) # grid of point
Z = z_func(X, Y) # evaluation of the function on the grid
fig,ax = plt.subplots()
ctr = ax.contourf( X,Y, Z)
xticks=np.linspace(xMin,xMax,9)
xtickLabels = [fr'{i:.1f}' for i in xticks]
ax.set_xticks(ticks=xticks, labels=xtickLabels)
ax.set_xlabel('x')
yticks = np.linspace( yMin,yMax, 9)
ytickLabels = [fr'{i:.1f}' for i in yticks]
ax.set_yticks(ticks=yticks, labels=ytickLabels)
ax.set_ylabel('y')
cbar = fig.colorbar(ctr, ax=ax, location='right')
cbar.ax.set_ylabel(r'$z=f(x,y)$')
Y = yticks**2
fig.subplots_adjust(left=0.20)
def y_forward(y):
return 1/y
# def forward(y):
# return 1/y
# def inverse(y):
# return y
secay=ax.secondary_yaxis('left', functions=(y_forward,y_forward))
secay.spines['left'].set_position(('outward', 40))
secay.set_ylabel(r'Y=1/y')
secay.yaxis.set_inverted(True)
fig.subplots_adjust(bottom=0.27)
def x_forward(x):
return 3*x
secax=ax.secondary_xaxis('bottom', functions=(x_forward,x_forward))
secax.spines['bottom'].set_position(('outward', 30))
secax.set_xlabel(r'X=3$\times$ x')
ax.grid(visible=False)
ax.set_title(r'$z=(1-x^2+y^3) e^{-(x^2+y^2)/2}$')
fig.tight_layout()
plt.show()
I think secondary_yaxis
and secondary_xaxis
generate their own scaling based on the transform functions. I would like each tick on the secondary axes to be linearly spaced. For example, if the primary y-axis has 9 tick values, I want the secondary y-axis to also have 9 tick values, each computed from a custom function applied to the primary y-axis values. Additionally, I want these ticks and values to be properly aligned so that the secondary axis values can be readily identified with their corresponding values on the primary axis. Can something like this be done using the secondary_yaxis
and secondary_xaxis
functions? Perhaps twinx
and twiny
would be useful ?
(Aka Axes and axis)
One important distinction to make here is between Axes and axis. That is quite confusing indeed.
But Axes
is the name of a matplotlib
class (the one of ax
), on which you can plot
things. While what you get with this SecondaryAxis
thing is just a new set of ticks and labels. But it is not a new Axes
, not a new place where you can plot things (even superposed to the same figure).
While twinx
and twiny
are that. They give a new Axes
(note that an Axes
is not about x or y. It is both). twinx
is a new Axes
, that shares the same x-axis with the original one. And have a brand new y-axis. Not just a brand new set of labels and ticks. the two axis (the one of ax
and the one of ax.twinx()
) are unrelated. If you let the drawing autoscale, they autoscale independently. You are not supposed to convert from one to another — not impossible, but that would require trying to care about the internals of where — at which pixel pos — matplotlib plots things.
So all that to say that, no twinx
and twiny
are off-topic here. twinx
and twiny
are on the contrary when you do not have conversion between the 2 axis on a figure. Like when you plot "temperature vs time" and "pressure vs time" on the same diagram (with 2 Axes
sharing the same "time" axis, but have two, with no conversion, y-axis. Obtained with twinx
)
So twinx
and twiny
are very useful, and it is worth understanding how they work. But they have nothing to do with what you want. Your "axis" are exact translation from one to another (said otherwise, they are not different axis. Y
or y
are the same thing. Just two ways to measure the same thing)
What you are trying to do is way simpler. You just want to control the ticks of Y
(and likewise X
)
You have set manually the ticks of the 2 main "axis" (between double quotes, just to emphasize they are not Axes
. But in plain English that is what people call "axis"). But you let matplotlib
do what it wants for the secondary axis. Just set their ticks to.
secay.set_yticks(y_forward(ax.get_yticks()))
secax.set_xticks(x_forward(ax.get_xticks()))
The two functions should be reciprocal. For y_forward
, since y↦1/y
happens to be its own inverse, no problem, this is already what you did. But for x_forward
it isn't. The inverse of lambda x:3*x
is lambda x:x/3
not itself. So for secax
should be secax=ax.secondary_xaxis('bottom', functions=(lambda x:3*x, lambda x:x/3))
not current secax=ax.secondary_xaxis('bottom', functions=(lambda x:3*x, lambda x:3*x))
(using lambda
notation here to avoid having to define another named fonction for this short explanation).
I don't know exactly what are the consequences of having a wrong inverse. Only thing I can see (by trial and error) is that if the inverse is decreasing when it should be increasing (or the other way around) then secondary axis is messed up. But other than that, for secax
whatever increasing function I pass as the second function of the pair, and for secay
whatever decreasing function (1/y
, -y
, ...) I pass as inverse function (second function of the pair), it seems that the graph is exactly the same.
But well, doc says the second should be the inverse of the other, so not following the doc is exposing yourself to UB, and loose right to complain if something goes wrong :D
You may have notice that the labels on my screen short are not perfect. But it is your labels (for primary axis) that are wrong, not by newly added ones :D
For example, for y-axis, matching 1.6 is 0.6400. Which is not accurate (1/1.6 is 0.625. When you are accustom to play with power of two, it strikes immediately that 2⁶/100 cannot be the same as 10/2⁴ :D).
Likewise, for x-axis 1.2×3 is not 3.562 (strangely it is the y-axis that I noticed first...)
But that is simply because of the way you computing your ticks and labels.
That 1.2 is the result of your linspace
, and is in reality 1.1875, that you associated with label 1.2
So, what to do is up to you
linspace
than ensures soround(1/round(x,1), 1)
is always the same as round(1/x, 1)
y2ticksLabels = [f'{1/round(y,1):.2f}' for y in yticks]
(so that the apparent error also disappear)Depends on how accurate you want things to be (and on how many reader do your thing would immediately notice "what? 1/1.6 isn't 0.64!")