Skip to content

Twinned axes do not allow setting of different formatters #7376

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
3 of 5 tasks
madphysicist opened this issue Nov 1, 2016 · 17 comments
Closed
3 of 5 tasks

Twinned axes do not allow setting of different formatters #7376

madphysicist opened this issue Nov 1, 2016 · 17 comments
Labels
status: inactive Marked by the “Stale” Github Action

Comments

@madphysicist
Copy link
Contributor

madphysicist commented Nov 1, 2016

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.
@efiring
Copy link
Member

efiring commented Nov 2, 2016

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.

@madphysicist
Copy link
Contributor Author

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.

@tacaswell
Copy link
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?

@efiring
Copy link
Member

efiring commented Nov 2, 2016

On 2016/11/02 5:25 AM, Thomas A Caswell wrote:

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?

I don't think that example helps--it is just using twinx, I believe, the
same as
http://matplotlib.org/examples/pylab_examples/multiple_yaxis_with_spines.html.

@madphysicist
Copy link
Contributor Author

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

@tacaswell tacaswell added this to the 2.1 (next point release) milestone Nov 6, 2016
@madphysicist
Copy link
Contributor Author

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.

@madphysicist
Copy link
Contributor Author

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

@madphysicist
Copy link
Contributor Author

madphysicist commented Nov 16, 2016

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.

@tacaswell
Copy link
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 🐑

@madphysicist
Copy link
Contributor Author

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.

@efiring
Copy link
Member

efiring commented Nov 28, 2016

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.

@madphysicist
Copy link
Contributor Author

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.

@madphysicist
Copy link
Contributor Author

madphysicist commented Nov 28, 2016

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

@efiring
Copy link
Member

efiring commented Nov 28, 2016

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

@tacaswell
Copy link
Member

@madphysicist What is the current state of this?

@tacaswell tacaswell modified the milestones: 2.1 (next point release), 2.2 (next next feature release) Sep 24, 2017
@github-actions
Copy link

github-actions bot commented Apr 1, 2023

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!

@github-actions github-actions bot added the status: inactive Marked by the “Stale” Github Action label Apr 1, 2023
@jklymak
Copy link
Member

jklymak commented Apr 1, 2023

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

@jklymak jklymak closed this as completed Apr 1, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
status: inactive Marked by the “Stale” Github Action
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants