Skip to content

Twinned axes do not allow setting of different formatters #7376

Closed

Description

@madphysicist

To help us understand and resolve your issue please check that you have provided
the information below.

  • Matplotlib version, Python version and Platform (Windows, OSX, Linux ...)
    Matplotlib 1.51, Python 3.5.2 on Red Hat Enterprise Linux 6.5

  • How did you install Matplotlib and Python (pip, anaconda, from source ...)
    Anaconda 1.5.1 (conda 4.2.9)

  • If possible please supply a Short, Self Contained, Correct, Example
    that demonstrates the issue i.e a small piece of code which reproduces the issue
    and can be run with out any other (or as few as possible) external dependencies.

from matplotlib import pyplot as plt
from matplotlib.ticker import Formatter, MaxNLocator

ax = plt.plot([0, 1, 2, 3], [0, 1, 2, 3])[0].axes
ax.figure.subplots_adjust(bottom=0.3)
ax.set_xlabel('Axis1')  # To demonstrate that labels can be set differently
ax2 = ax.twinx()
ax2.set_frame_on(True)
ax2.patch.set_visible(False)
ax2.yaxis.set_visible(False)
ax2.xaxis.set_visible(True)
ax2.spines['bottom'].set_position(('axes', -0.2))  # So far so good
ax2.set_xlabel('Axis2')  # Surprisingly (or not), this works

class F(Formatter):
    def __call__(self, x, pos=None):
       return '%03d' % x

# This does not work as expected:
ax.xaxis.set_major_locator(MaxNLocator(integer=True))  # Both locators get modified
ax2.xaxis.set_major_formatter(F())  # Both formatters get modified

I suspect that this has something to do with the fact that Axis has a Ticker container for the attributes that are behaving inappropriately, and that they are not being copied correctly when the axis is twinned for some reason. I have not been able to trace this problem down yet, but with a few pointer from the experts, I'd probably be able to make a PR. Assuming this is actually an issue, of course.

  • If this is an image generation bug attach a screenshot demonstrating the issue.
  • If this is a regression (Used to work in an earlier version of Matplotlib), please
    note where it used to work.

Activity

  1. efiring commented on Nov 2, 2016

    @efiring
    Member

    This is inherent in the way axis sharing is defined and implemented: for all shared axes, there is a single major formatter, major locator, etc. Twinx uses axis sharing, so there is no way to get the behavior you are looking for with twinx. Instead, you need to make a second axes without sharing, but with its view limits taken from the original axes, and tracking the original axes via a callback. I'm sorry I can't provide an example offhand. The nearest thing I have found in the gallery is the viewlims.py example. It would be nice to have a simpler example.

  2. madphysicist commented on Nov 2, 2016

    @madphysicist
    ContributorAuthor

    I will take a look at your example and use it as a workaround. Is it possible/desirable to dissociate the locator and formatter at all, or will it totally break axis sharing? I don't mind working on a PR if it is conceptually possible.

  3. tacaswell commented on Nov 2, 2016

    @tacaswell
    Member

    The point on twinx and twiny is that the shared axis are (in the sense of assert a is b) the same.

    In [50]: ax = plt.gca()
    
    In [51]: ax2 = ax.twinx()
    
    In [52]: ax.xaxis is ax2.xaxis
    Out[52]: False
    
    In [53]: ax.xaxis.get_major_locator() is ax2.xaxis.get_major_locator()
    Out[53]: True
    
    

    This also has implications at draw time to make sure we do not double-draw the axis parts.

    You might want to use http://matplotlib.org/mpl_toolkits/axes_grid/users/overview.html#axisartist-with-parasiteaxes as it looks like you really just want a second xaxis off set by a bit with different units?

  4. madphysicist commented on Nov 6, 2016

    @madphysicist
    ContributorAuthor

    @tacaswell What confuses me about your statement is that you say that the twinned axes are supposed to be the same. However, I am noticing that almost all of their properties can be modified completely independently (see the lines with comments # So far so good and # Surprisingly (or not), this works in the initial code sample.

    A very cursory look at the code and behavior makes it seem that the only reason this does not apply to the Locator and Formatter instances is that they are contained in an object of type Ticker rather than directly as an attribute of the axis. If a shallow copy is made, both axes end up with the same Ticker reference. I believe that the Ticker objects should be copied shalowly when the axes are twinned (not 100% shallow copy of the axis), and that that was probably the original intention.

    The problem is that I do not know if my theory is correct or where to insert the code if it is.

  5. added this to the milestone on Nov 6, 2016
  6. madphysicist commented on Nov 15, 2016

    @madphysicist
    ContributorAuthor

    I have attempted to trace what happens when I do ax.twinx(). I still do not have a clear picture of where the Axis objects are cloned, but here is what I have so far in terms of the relevant call sequence:

    1. axes._AxesBase.twinx() calls self._make_twin_axes(sharex=self)
    2. axes._AxesBase._make_twin_axes calls self.figure.add_axes(self.get_position(True), *kl, **kwargs), with kl=() and kwargs={'sharex': self}.
    3. Figure.add_axes calls a = projection_class(self, rect, **kwargs) with projection_class == axes.Axes and kwargs={'sharex': self}. I am pretty sure that this is the branch that will be called because projections.process_projection_requirements (called from here) does not do anything special with the sharex keyword (the key is actually made by Figure._make_key), meaning that that returned key will be distinct and a new Axes needs to be made.
    4. axes.Axes.__init__ is not defined, so it falls back to axes._AxesBase.__init__
    5. axes._AxesBase.__init__ does not appear to do much with the sharex keyword. There is a branch that adds the shared axes to the appropriate grouper and sets its _adjustable attribute. There is also a call to self._init_axis() which appears to make a new XAxis object. However, I do not see any calls that explicitly copy the Axis of the base Axes.

    Where do the Axis properties get copied over? I am clearly missing something crucial here.

    Line numbers are references to the latest commit to go into master at time of writing.

  7. madphysicist commented on Nov 15, 2016

    @madphysicist
    ContributorAuthor

    @tacaswell With reference to your example, the issue is that ax.xaxis.major is ax2.xaxis.major. This should not be the case. What should be the case, at least initially, is that ax.xaxis.major.locator is ax2.xaxis.major.locator and ax.xaxis.major.formatter is ax2.xaxis.major.formatter. That is what happens to the other attributes.

  8. madphysicist commented on Nov 16, 2016

    @madphysicist
    ContributorAuthor

    An update to my original post that definitively illustrates the issue: I have written a function to copy the Axis.major and Axis.minor axis.Ticker objects and the example I provided works just fine:

    from matplotlib.axis import Ticker
    
    
    def copyTicker(axis, which='both'):
        """
        Makes an exact copy of the `matplotlib.axis.Ticker` objects of an axis.
    
        The purpose of this method is to allow twinned axes to not share their
        locator and formatters via a common Ticker reference.
        """
        def copy(ticker):
            t = Ticker()
            t.locator = ticker.locator
            t.formatter = ticker.formatter
            return t
    
        if which in ('both', 'major'):
            axis.major = copy(axis.major)
        if which in ('both', 'minor'):
            axis.minor = copy(axis.minor)

    Now I can do

    from matplotlib import pyplot as plt
    from matplotlib.ticker import Formatter, MaxNLocator
    
    ax = plt.plot([0, 1, 2, 3], [0, 1, 2, 3])[0].axes
    ax.figure.subplots_adjust(bottom=0.3)
    ax.set_xlabel('Axis1')  # To demonstrate that labels can be set differently
    ax2 = ax.twinx()
    ax2.set_frame_on(True)
    ax2.patch.set_visible(False)
    ax2.yaxis.set_visible(False)
    ax2.xaxis.set_visible(True)
    ax2.spines['bottom'].set_position(('axes', -0.2))  # So far so good
    ax2.set_xlabel('Axis2')  # Surprisingly (or not), this works
    
    class F(Formatter):
        def __call__(self, x, pos=None):
           return '%03d' % x
    
    # This does! work as expected:
    copyTicker(ax.xaxis)
    ax.xaxis.set_major_locator(MaxNLocator(integer=True))
    ax2.xaxis.set_major_formatter(F())

    The result is exactly as expected:

    test

    Basically, I am asking for help to figure out where I can insert something like my copyTicker function into the Axes.twinx pipeline.

  9. tacaswell commented on Nov 17, 2016

    @tacaswell
    Member

    I do not have the bandwidth to have anything sensible to say about this tonight other than I apparently do not actually understand how twinx and twiny work 🐑

  10. madphysicist commented on Nov 28, 2016

    @madphysicist
    ContributorAuthor

    Just wanted to document that I think I found the solution. Continuing from my previous comment:

    1. axes._AxesBase.__init__ sets self._sharex and self._sharey as part of the initialization.
    2. It then calls self.cla.
    3. axes._AxesBase.cla makes a half-shallow copy of any shared x-axis elements. Same for y-axis.
    4. As I mentioned earlier, the copy copies the references to the actual Ticker objects instead of allocating a new container and copying the contents shalowly (major and minor).

    The fix is nearly trivial, pretty much exactly as my followup comment indicates. PR coming very soon.

  11. efiring commented on Nov 28, 2016

    @efiring
    Member

    I haven't looked at this closely, but it seems like there are two different possible behaviors depending on the depth of the copy, and there may be legitimate use cases for both. Therefore we need to be very careful that the change you propose doesn't break existing code relying on the present design. It might be that your proposal needs to be an option or alternative, not a replacement.

  12. madphysicist commented on Nov 28, 2016

    @madphysicist
    ContributorAuthor

    Agreed. I will rerun all the tests to check for regressions. I will submit my PR as-is for now and think about how to add a keyword option for sharing a common ticker instance vs. the contents of it.

  13. madphysicist commented on Nov 28, 2016

    @madphysicist
    ContributorAuthor

    What would be a good keyword name to determine if the ticker gets copied by value or by reference? I am thinking either dual (short but obscure) or share_tickers (probably more explicit than it needs to be).

  14. efiring commented on Nov 28, 2016

    @efiring
    Member

    'share_tickers' sounds right to me.
    Possible problem with not sharing the locator: there could be an autoscaling conflict, because autoscaling can involve calls to the locator. In that case, which one wins?

  15. tacaswell commented on Sep 24, 2017

    @tacaswell
    Member

    @madphysicist What is the current state of this?

  16. modified the milestones: , on Sep 24, 2017
  17. github-actions commented on Apr 1, 2023

    @github-actions

    This issue has been marked "inactive" because it has been 365 days since the last comment. If this issue is still present in recent Matplotlib releases, or the feature request is still wanted, please leave a comment and this label will be removed. If there are no updates in another 30 days, this issue will be automatically closed, but you are free to re-open or create a new issue if needed. We value issue reports, and this procedure is meant to help us resurface and prioritize issues that have not been addressed yet, not make them disappear. Thanks for your help!

  18. jklymak commented on Apr 1, 2023

    @jklymak
    Member

    This is now provided via secondary_xaxis, unless my skim of the issue was too quick.

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

    status: inactiveMarked by the “Stale” Github Action

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions