FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

plotting using secondary axis with mpl_toolkits.axes_grid1.parasite_axes.SubplotHost().twin() produces very weird outputs · Issue #7258 · matplotlib/matplotlib · GitHub

Repository navigation

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

Closed

Description

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:

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.

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.

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:

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

Activity

  1. griai commented on Oct 12, 2016

    Author

    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()?

  2. added this to the milestone on Oct 12, 2016
  3. tacaswell commented on Oct 12, 2016

    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/

  4. griai commented on Oct 12, 2016

    Author

    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.

  5. phobson commented on Oct 12, 2016

    Member

    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))
  6. phobson commented on Oct 12, 2016

    Member

    @GRitschel I updated the example above. When I run it, it looks right to me. I'm curious what you think.

  7. Kojoley commented on Oct 12, 2016

    Member

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

  8. phobson commented on Oct 12, 2016

    Member

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

  9. griai commented on Oct 14, 2016

    Author

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

  10. griai commented on Oct 14, 2016

    Author

    @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?

  11. Kojoley commented on Oct 14, 2016

    Member

    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`.
  12. phobson commented on Oct 14, 2016

    Member

    @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
    
  13. griai commented on Oct 17, 2016

    Author

    @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?

  14. modified the milestones: , on Oct 3, 2017
  15. dstansby commented on May 12, 2019

    Member

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

  16. jklymak commented on May 12, 2019

    Member

    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.

  17. modified the milestones: needs sorting, on May 13, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions


      Back | FazBrowse Home | New Git URL