Skip to content

plotting using secondary axis with mpl_toolkits.axes_grid1.parasite_axes.SubplotHost().twin() produces very weird outputs #7258

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
griai opened this issue Oct 12, 2016 · 14 comments

Comments

@griai
Copy link

griai commented Oct 12, 2016

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.)

Using it with

fig = plt.figure()
ax = SubplotHost(fig, 1,1,1)
fig.add_subplot(ax)

ax2 = transformed_x_axis(ax, lambda x: 1. / x)

x = np.linspace(1., 2., 200)
ax.plot(x, np.sin(x))
plt.show()

produces exactly what I want -- a plot with a secondary transformed x axis:
01

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:
02

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:
03

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.

fig = plt.figure()
ax = SubplotHost(fig, 1,1,1)
fig.add_subplot(ax)

ax2 = transformed_x_axis(ax, lambda x: np.exp(x))

x = np.linspace(1., 5., 200)
ax.plot(x, np.sin(x))
plt.show()

gives me a RuntimeError in an IPython Notebook and in a terminal it produces a secondary axis that contains nothing but the value 0.
04

For other data limits,

ax2 = transformed_x_axis(ax, lambda x: np.exp(x))
x = np.linspace(1., 2., 200)
ax.plot(x, np.sin(x))

I get a MemoryError in the notebook. When done in the terminal, I see no secondary x axis.

The strangest things occur for simple linear transformations:

fig = plt.figure()
ax = SubplotHost(fig, 1,1,1)
fig.add_subplot(ax)

ax2 = transformed_x_axis(ax, lambda x: 2. * x)

x = np.linspace(1., 2., 200)
ax.plot(x, np.sin(x))
plt.show()

Here, in the terminal, I see, again, no secondary axis. But in the notebook, I think one can see that something goes dramatically wrong:
05

I think, it is the ticker that does those weird things when it faces transformed axes.

Anyway, the behavior is inconsistent, unexpected and very weird.

Cheers, Gerhard


Python 2.7.12, matplotlib 1.5.1 on Windows 64 bit
everything installed via Anaconda

@griai
Copy link
Author

griai commented 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 tacaswell added this to the 2.1 (next point release) milestone Oct 12, 2016
@tacaswell
Copy link
Member

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
Copy link
Author

griai commented 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
Copy link
Member

phobson commented 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):

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, fwds, bkwds):

    class MyTransform(mtransforms.Transform):
        input_dims = 1
        output_dims = 1
        is_separable = False
        has_inverse = False

        def __init__(self, fwds, bkwds):
            mtransforms.Transform.__init__(self)
            self.fwds = fwds
            self.bkwds = bkwds

        def transform_non_affine(self, x):
            return self.fwds(x)

        def inverted(self):
            return MyInvertedTransform(self.bkwds, self.fwds)

    class MyInvertedTransform(MyTransform):
        def inverted(self):
            return MyTransform(self.bkwds, self.fwds)

    aux_trans = mtransforms.BlendedGenericTransform(
            MyTransform(fwds, bkwds), mtransforms.IdentityTransform())

    ax2 = ax.twin(aux_trans)
    ax2.set_viewlim_mode("transform")
    ax2.axis["right"].toggle(ticklabels=False)
    return ax2

if __name__ == '__main__':
    fig = plt.figure()
    ax = SubplotHost(fig, 1,1,1)
    fig.add_subplot(ax)

    ax2 = transformed_x_axis(ax, lambda x: 2. * x, lambda x: 0.5 * x)
    x = np.linspace(1., 2., 200) 
    ax.plot(x, np.sin(x))

@phobson
Copy link
Member

phobson commented 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
Copy link
Member

Kojoley commented 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.

the ticker has problems and draws ticklabels on top of each other

It is just lack of the space
issue_7258_8_6

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

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.
issue_7258

@phobson
Copy link
Member

phobson commented 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
Copy link
Author

griai commented 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 expected

I have no idea why this would happen. It seems like a bug to me.

@griai
Copy link
Author

griai commented 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.

06

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

07

ax2 = transformed_x_axis(ax, lambda x: 0.5 / x, lambda x: 2. / x) produces

08

Both, again, show issues with the ticker, I think. Can someone explain this behaviour?

@Kojoley
Copy link
Member

Kojoley commented Oct 14, 2016

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.

class Transform(TransformNode):
    """
    ...
    All non-affine transformations should be subclasses of this class.
    New affine transformations should be subclasses of
    :class:`Affine2D`.

@phobson
Copy link
Member

phobson commented 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:

In [1]: fwd = lambda x: 2. / x

In [2]: bkd = lambda x: 0.5 / x

In [3]: fwd(bkd(4))
Out[3]: 16.0

@griai
Copy link
Author

griai commented 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?

@tacaswell tacaswell modified the milestones: 2.1 (next point release), 2.2 (next next feature release) Oct 3, 2017
@dstansby
Copy link
Member

@gritschel is there any chance you could check if this is still an issue with the latest version of Matplotlib (3.0.3)?

@jklymak
Copy link
Member

jklymak commented 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.

@jklymak jklymak closed this as completed May 12, 2019
@QuLogic QuLogic modified the milestones: needs sorting, unassigned May 13, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

7 participants