For several plots I'd like to have a secondary x axis that is a non-affine transformation of the original axis. Following a very old discussion on nabble I recognized that it is only possible, if I define my own matplotlib.transforms.Transform() class or I have to set tick locations manually according to a discussion on stackoverflow. I managed to make the code from the nabble link work, but I had to rename the class method transform() to transform_non_affine(). Otherwise it would not work.
So I now have the following code, which seemed to work fine for the first few tests.
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.transforms as mtransforms
from mpl_toolkits.axes_grid1.parasite_axes import SubplotHost
def transformed_x_axis(ax, transformation):
class MyTransform(mtransforms.Transform):
input_dims = 1
output_dims = 1
is_separable = False
has_inverse = False
def transform_non_affine(self, x):
return transformation(x)
def inverted(self):
return MyInvertedTransform()
class MyInvertedTransform(MyTransform):
def inverted(self):
return MyTransform()
aux_trans = mtransforms.BlendedGenericTransform(
MyTransform(), mtransforms.IdentityTransform())
ax2 = ax.twin(aux_trans)
ax2.set_viewlim_mode("transform")
ax2.axis["right"].toggle(ticklabels=False)
return ax2
For convenience I defined a function 'transformed_x_axis()' that just accepts an original matplotlib.axes.Axes object and a transformation formula.
(Actually, I think that my use case is pretty common and matplotlib should provide such kind of function that I tried to use, here.)
produces exactly what I want -- a plot with a secondary transformed x axis:
Unfortunately, for other transformations or for other data limits I see all kinds of weirdest things happening.
For other data ranges, e.g. x = np.linspace(1., 5., 200), the ticker has problems and draws ticklabels on top of each other:
If the axis approaches the critical point 0.0 of my transformation, the ticks don'e even fill the axis but only a tiny part of it:
I guess that this is a result of the linear scale of the ticklabels. It seems the ticks for the axis are chosen by value and not by position in axes coordinates, which I find kind of wrong.
Other transformations also produce weird things, e.g.
Side question: Can this, what I wanted to accomplish, be done in an easier way? Maybe without relying on mpl_toolkits.axes_grid1.parasite_axes.SubplotHost().transform()?
You also need to define a Locator class to determine where to put the ticks (or just use FixedLocator to hard code where they are).
It is feature that the ticks are located in data space (rather than in screen space) so that it is easy to put them at meaning full numbers (ex, evenly spaced in screen space ticks on a log scale would look weird).
Thank you very much for the quick answer! And thanks for the link.
I understand what you are saying. But isn't it nevertheless strange behavior that the ticker puts the ticks outside the axes as in the last one of my examples?
If you are saying, everything is working exactly as expected, then please regard my report as a feature request for easier axis transformations. I guess that many people would like to have a higher-level function for what I was trying.
@phobson for first three plots he uses 1. / x and the inverse of it is the same 1. / x
@GRitschel I don not see any bugs for the first two plots.
the ticker has problems and draws ticklabels on top of each other
It is just lack of the space
Your last plot with @phobson's transformed_x_axis (ax2 = transformed_x_axis(ax, np.exp, np.log)) is:
Please provide the code for the third plot.
I have RuntimeWarning: divide by zero encountered in true_divide and shifted ticks with np.linspace(0.1, 5., 200) so may be this is the problem of the third image.
@GRitschel The problem with the third plot is that the Locator for the scale doesn't know about the transformation. So when ax1's locator says the ticks need to be at 1, 2, ..., 5, ax2 tries to do the same thing, but 1/x will always cram everything over on the left side since 1/x blows up as x -> 0.
@phobson Thank you very much for the advice with the inverse transformation! That fixed essentially everything. Funny enough, it also works, if the attribute has_inverse is False. Why could that be? Also, I would suggest, that mpl should somehow complain, that it needs an inverse transformation, too. It is (at least from my naive point of view) not clear why this should be necessary at all.
@Kojoley You're right, in the second plot, the only problem seems to be lack of space. Of course, if my transformation is 1 / x, then zero should not be part of the original axis.
What I was also wondering about is the fact that matplotlib.transforms.BlendedGenericTransform() needs the transformations to be defined with the method transform_non_affine() in my examples. Although the transformations obviously also work with the method transform() only, the blended transformation then only outputs the input and not -- well -- the blended transformation.
(This is true even for trivial affine transformations like 2. * x.)
Please, see the code below:
class MyTransform(mtransforms.Transform):
input_dims = 1
output_dims = 1
is_separable = False
has_inverse = False
def transform(self, x):
return 2. * x
bld_trans = mtransforms.BlendedGenericTransform(
MyTransform(), mtransforms.IdentityTransform())
print(MyTransform().transform(0.5))
print(bld_trans.transform([0.5, 0.5]))
# 1.0
# [0.5, 0.5]
# --> does not work as expected
... and compare this to:
class MyTransform(mtransforms.Transform):
input_dims = 1
output_dims = 1
is_separable = False
has_inverse = False
def transform_non_affine(self, x):
return 2. * x
bld_trans = mtransforms.BlendedGenericTransform(
MyTransform(), mtransforms.IdentityTransform())
print(MyTransform().transform(0.5))
print(bld_trans.transform([0.5, 0.5]))
# 1.0
# [1.0, 0.5]
# --> works as expected
I have no idea why this would happen. It seems like a bug to me.
@phobson I checked your code again and there is still a subtle point, which I didn't recognize at first. If I run your example code, all Ticker problems are gone, but the transformation is done in the exact opposite direction! This I don't understand.
The important part being ax2 = transformed_x_axis(ax, lambda x: 2. * x, lambda x: 0.5 * x), so the forward transformation should have been 2. * x, not 0. 5 * x (as in the plot).
Also, if I'm using 1 / x with an additional factor, things become weird again:
I have no idea why this would happen. It seems like a bug to me.
The transformation part of matplotlib is complicated. Affine transformations must subclass of Affine2D because other parts (e.g. BlendedGenericTransform) relies on its transformation matrix.
@phobson Oh, how stupid of me. It seems I did this too quickly. You're right, of course. With the proper back-transformation the tick problems do not occur anymore. Do you have an explanation for the wrong direction of transforms that I wrote about in my last post? Could this be the explanation, why the back-transformation is needed in the first place? From a theoretical point of view, I don't see why the back-transformation should be needed. Or what else could I be missing?
3.1 will also have this support baked in natively: #11859 I'm going to close this as obsolete, but feel free to open a new issue if there is problems with the new functionality.
Activity
griai commented on Oct 12, 2016
Side question: Can this, what I wanted to accomplish, be done in an easier way? Maybe without relying on mpl_toolkits.axes_grid1.parasite_axes.SubplotHost().transform()?
tacaswell commented on Oct 12, 2016
You also need to define a Locator class to determine where to put the ticks (or just use FixedLocator to hard code where they are).
It is feature that the ticks are located in data space (rather than in screen space) so that it is easy to put them at meaning full numbers (ex, evenly spaced in screen space ticks on a log scale would look weird).
@phobson has done a fair amount of work with non-linear scales at https://phobson.github.io/mpl-probscale/
griai commented on Oct 12, 2016
Thank you very much for the quick answer! And thanks for the link.
I understand what you are saying. But isn't it nevertheless strange behavior that the ticker puts the ticks outside the axes as in the last one of my examples?
If you are saying, everything is working exactly as expected, then please regard my report as a feature request for easier axis transformations. I guess that many people would like to have a higher-level function for what I was trying.
phobson commented on Oct 12, 2016
I think one issue here is that your Inverted Transform is incomplete.
If you're going to have a transform that is lambda x: x * 2.0, then you'll also need an inversion that's lambda x: x * 0.5.
So I think your example would become (untested):
phobson commented on Oct 12, 2016
@GRitschel I updated the example above. When I run it, it looks right to me. I'm curious what you think.
Kojoley commented on Oct 12, 2016
@phobson for first three plots he uses 1. / x and the inverse of it is the same 1. / x
@GRitschel I don not see any bugs for the first two plots.
It is just lack of the space

Your last plot with @phobson's transformed_x_axis (ax2 = transformed_x_axis(ax, np.exp, np.log)) is:

Please provide the code for the third plot.
I have RuntimeWarning: divide by zero encountered in true_divide and shifted ticks with np.linspace(0.1, 5., 200) so may be this is the problem of the third image.

phobson commented on Oct 12, 2016
@GRitschel The problem with the third plot is that the Locator for the scale doesn't know about the transformation. So when ax1's locator says the ticks need to be at 1, 2, ..., 5, ax2 tries to do the same thing, but 1/x will always cram everything over on the left side since 1/x blows up as x -> 0.
griai commented on Oct 14, 2016
@phobson Thank you very much for the advice with the inverse transformation! That fixed essentially everything. Funny enough, it also works, if the attribute has_inverse is False. Why could that be? Also, I would suggest, that mpl should somehow complain, that it needs an inverse transformation, too. It is (at least from my naive point of view) not clear why this should be necessary at all.
@Kojoley You're right, in the second plot, the only problem seems to be lack of space. Of course, if my transformation is 1 / x, then zero should not be part of the original axis.
What I was also wondering about is the fact that matplotlib.transforms.BlendedGenericTransform() needs the transformations to be defined with the method transform_non_affine() in my examples. Although the transformations obviously also work with the method transform() only, the blended transformation then only outputs the input and not -- well -- the blended transformation.
(This is true even for trivial affine transformations like 2. * x.)
Please, see the code below:
class MyTransform(mtransforms.Transform): input_dims = 1 output_dims = 1 is_separable = False has_inverse = False def transform(self, x): return 2. * x bld_trans = mtransforms.BlendedGenericTransform( MyTransform(), mtransforms.IdentityTransform()) print(MyTransform().transform(0.5)) print(bld_trans.transform([0.5, 0.5])) # 1.0 # [0.5, 0.5] # --> does not work as expected... and compare this to:
class MyTransform(mtransforms.Transform): input_dims = 1 output_dims = 1 is_separable = False has_inverse = False def transform_non_affine(self, x): return 2. * x bld_trans = mtransforms.BlendedGenericTransform( MyTransform(), mtransforms.IdentityTransform()) print(MyTransform().transform(0.5)) print(bld_trans.transform([0.5, 0.5])) # 1.0 # [1.0, 0.5] # --> works as expectedI have no idea why this would happen. It seems like a bug to me.
griai commented on Oct 14, 2016
@phobson I checked your code again and there is still a subtle point, which I didn't recognize at first. If I run your example code, all Ticker problems are gone, but the transformation is done in the exact opposite direction! This I don't understand.
The important part being ax2 = transformed_x_axis(ax, lambda x: 2. * x, lambda x: 0.5 * x), so the forward transformation should have been 2. * x, not 0. 5 * x (as in the plot).
Also, if I'm using 1 / x with an additional factor, things become weird again:
ax2 = transformed_x_axis(ax, lambda x: 2. / x, lambda x: 0.5 / x) produces
ax2 = transformed_x_axis(ax, lambda x: 0.5 / x, lambda x: 2. / x) produces
Both, again, show issues with the ticker, I think. Can someone explain this behaviour?
Kojoley commented on Oct 14, 2016
The transformation part of matplotlib is complicated. Affine transformations must subclass of Affine2D because other parts (e.g. BlendedGenericTransform) relies on its transformation matrix.
phobson commented on Oct 14, 2016
@GRitschel I think your ticks are messed up b/c your math is wrong.
fwd(bwkd(x)) should be equal to x.
In your example:
griai commented on Oct 17, 2016
@phobson Oh, how stupid of me. It seems I did this too quickly. You're right, of course. With the proper back-transformation the tick problems do not occur anymore. Do you have an explanation for the wrong direction of transforms that I wrote about in my last post? Could this be the explanation, why the back-transformation is needed in the first place? From a theoretical point of view, I don't see why the back-transformation should be needed. Or what else could I be missing?
dstansby commented on May 12, 2019
@GRitschel is there any chance you could check if this is still an issue with the latest version of Matplotlib (3.0.3)?
jklymak commented on May 12, 2019
3.1 will also have this support baked in natively: #11859 I'm going to close this as obsolete, but feel free to open a new issue if there is problems with the new functionality.