Skip to content

document event handling with twined axes #10009

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
darioflute opened this issue Dec 14, 2017 · 14 comments · Fixed by #25555
Closed

document event handling with twined axes #10009

darioflute opened this issue Dec 14, 2017 · 14 comments · Fixed by #25555
Milestone

Comments

@darioflute
Copy link

Bug report

Bug summary

In matplotlib 2.1.0 it was possible to define a SpanSelector attached to an axes object,
then define another axes system connected to the first one with twinx or twiny.
The SpanSelector was still active and returning the values on the first axes object.

With matplotlib 2.1.1 this is no more working.
The SpanSelector works only if attached to the latest axes object defined.

The same effect happens also when using draggable annotate attached to an axes object.
After defining a twin axes, the annotations are no more draggable.

Code for reproduction

As an example, I include a slight modification of the matplotlib tutorial about the usage
of span_selector:
https://matplotlib.org/examples/widgets/span_selector.html

At the end of the code I added twin axes which neutralize the span selector.
After uncommenting the last three lines, the span selector works again.
With matplotlib 2.1.0, the code was working without uncommenting the lines.

#!/usr/bin/env python
"""
The SpanSelector is a mouse widget to select a xmin/xmax range and plot the
detail view of the selected region in the lower axes
"""
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.widgets import SpanSelector

fig = plt.figure(figsize=(8, 6))
ax = fig.add_subplot(211, facecolor='#FFFFCC')

x = np.arange(0.0, 5.0, 0.01)
y = np.sin(2*np.pi*x) + 0.5*np.random.randn(len(x))

ax.plot(x, y, '-')
ax.set_ylim(-2, 2)
ax.set_title('Press left mouse button and drag to test')


ax2 = fig.add_subplot(212, facecolor='#FFFFCC')
line2, = ax2.plot(x, y, '-')


def onselect(xmin, xmax):
    indmin, indmax = np.searchsorted(x, (xmin, xmax))
    indmax = min(len(x) - 1, indmax)
    print(indmin,indmax)

    thisx = x[indmin:indmax]
    thisy = y[indmin:indmax]
    line2.set_data(thisx, thisy)
    ax2.set_xlim(thisx[0], thisx[-1])
    ax2.set_ylim(thisy.min(), thisy.max())
    fig.canvas.draw()

# set useblit True on gtkagg for enhanced performance
span = SpanSelector(ax, onselect, 'horizontal', useblit=True,
                    rectprops=dict(alpha=0.5, facecolor='red'))

ax1 = ax.twinx()
ax1.get_yaxis().set_tick_params(labelright='on',right='on', direction='in')
ax1.set_ylim([0,1])

# Uncomment here to work with matplotlib 2.1.1 (possible bug)
#span = SpanSelector(ax1, onselect, 'horizontal', useblit=True,
#                    rectprops=dict(alpha=0.5, facecolor='red'))


plt.show()

Actual outcome

The span selector does not work.

# If applicable, paste the console output here
#
#

Expected outcome

I expected that the spanselector would work.

This was working with matplotlib 2.1.0

Matplotlib version

  • Operating system: mac os-x
  • Matplotlib version: 2.1.1
  • Matplotlib backend (print(matplotlib.get_backend())):
  • Python version: 3.6.1
  • Jupyter version (if applicable):
  • Other libraries:

conda

default

@efiring
Copy link
Member

efiring commented Dec 14, 2017

The problem here is that only one Axes gets the events, and it is hard to figure out which one that will be. I didn't realize it had changed between 2.1 and 2.1.1, though--that's really bad! I tried to address an aspect of this in #2986; there, @tacaswell pointed out that the real solution would be to let all Axes receive all events, but I don't think any of us ever followed through on that.

@jklymak
Copy link
Member

jklymak commented Dec 14, 2017

Confirmed: This worked in 2.1.0, but doesn't work now. Checked qt5Agg and maxosx backends, so must be somewhere generic.

@jklymak
Copy link
Member

jklymak commented Dec 14, 2017

This is almost certainly #9389

I'm not sure the behaviour is incorrect per-se here, unless the docs somewhere specify what order events are to be received in. ax1 was drawn last so it gets the events. I'll let @anntzer chime in but it seems if you want to garauntee ax gets the events, either draw it last, or set its zorder larger than ax1.

As @efiring says, maybe it'd make sense to let both axes get the events, but that seems ripe for confusion as well..

@anntzer
Copy link
Contributor

anntzer commented Dec 14, 2017

As long as only one axes can receive the event, I strongly believe it is the topmost one that should get them.

@jklymak
Copy link
Member

jklymak commented Dec 14, 2017

I guess one could argue that twinx should put the twinned axes with a lower z-order than the original axes.

@darioflute
Copy link
Author

Thanks @jklymak,

a simple solution as you suggest, is to add the lines:

ax.set_zorder(ax1.get_zorder()+1)
ax.patch.set_visible(False)

This solution works for matplotlib 2.1.0 and 2.1.1.

@thriveth
Copy link

Confirmed, this also broke my code after the latest update.

I do not understand the underlying workings of matplotlib event handling, but it seems quite counterintuitive that when you assign a SpanSelector to a given axes object, it's another axes object that actually receives the event. My immediate expectation would be that the axes I assigned to the widget is also the one that is listening to it.

@tacaswell
Copy link
Member

Currently only one Axes, the 'top' one, gets mouse and keyboard events when the Axes overlap (which makes sense if they are are opaque). Twin axes adds an overlapping Axes with a transparent patch (so you can see the Axes below it) at the same z-order as the original Axes. When sorting out what the 'top' and there is a tie we have to break that tie some how. In <2.1.0 we took the last Axes (consistent with the draw order), in 2.1.0 we flipped that to take the first (which was a bug) and via #9389 return to picking the last in 2.1.1.

I think the TODO here add documentation to the widgets page and/or the event handling page to explain this and the workaround above.

Removed the GUI specific tags as the issues are all in Mpls handling, not the GUIs.

Also: https://xkcd.com/1172/

@tacaswell tacaswell changed the title Adding an axis with twinx renders ineffective SpanSelector document event handling with twined axes Feb 23, 2018
@thriveth
Copy link

Hi @tacaswell ,

When a SpanSelector is initialized, it takes an Axes object as the first positional argument. I don't think it is unreasonable to then expect the widget to be connected to that Axes object, and I think it is reasonable to expect that it does not quietly start listening to a different Axes object that is added later. Otherwise, what is the point of assigning it an Axes object in the first place?

Not only is that the behavior I would intuitively expect, I also think it is better in line with an object-oriented design philosophy rather than procedural/scripting philosophy. After all, scripting and interactive widgets are not exactly an optimal match.

Example: I use Matplotlib to perform a number of interactive operations on redshifted astronomical spectra. The base plot here is observed wavelength vs. flux density. For various reasons, I often want an alternative x-axis shown -- rest frame wavelength, frequency, velocity, etc., and I want it to be easy to toggle these. But I want any interactions I perform to still take place in obs-wavelength space, and that is what I expect to happen when that axis is the one I assigned to my SpanSelector call in the first place. In an interactive application, it should not matter in which order things are added or removed; when a listening channel has been explicitly declared between a widget and an Axes object, it should stay consistent or at the very least break verbosely by throwing an error, not just stealthily be overtaken or blocked.

Also, please don't be rude. I don't think the OPs or my expectations to how things should work are crazy, and I certainly don't think they deserve being mocked for hanging on to horribly buggy behavior out of sheer habit. I think there's a design flaw here and I think it merits being discussed in earnest.

@jklymak
Copy link
Member

jklymak commented Feb 23, 2018

@thriveth I think @tacaswell was just trying to be funny, not rude. He works extremely hard on this project, and is extremely gracious with users.

As pointed out above, the "widget" does not receive the events, the axes do, and then they pass events to any widgets that are registered with them. The issue here is that the wrong axes is capturing the event, and then not doing anything w/ it.

It should be documented.

It could be fixed, but someone would need to spend the time to do it, and because it involves passing events around between axes, it could break other things. To make matters worse, its very hard for us to routinely test interactive behaviour. At the very least this change would require a bit of engineering and planing (i.e. whats the proper API if there is an inset axis or partially overlapping axis?)

If you or someone else wanted to craft a plan to deal with this, or write a PR to do so, I'm sure the core devs would be thrilled, and would extend you every assistance. But given that time is short, and that there is a pretty clear workaround, a doc change is perhaps all that will be made in the near term.

However, no one is dismissing this issue. The fact that @efiring, @tacaswell and @anntzer all chimed in on this indicates that everyone is taking it seriously.

Thanks for your patience!

@tacaswell tacaswell modified the milestones: v3.0, v3.1 Aug 11, 2018
@jklymak jklymak modified the milestones: v3.1.0, v3.2.0 Feb 27, 2019
@tacaswell tacaswell modified the milestones: v3.2.0, needs sorting Sep 10, 2019
@tacaswell
Copy link
Member

@thriveth Sorry, this got lost in my mentions. I would like to apologize (2 years on). I am sure I did not intend to be rude or mock you, it was meant to be self-deprecating humor on how every change to a library the size / complexity of Matplotlib, even ones that seem like they are obviously bug-fixes are going to break someone (despite our best attempts). I'll try to refrain from making that joke in the future.

@thriveth
Copy link

@tacaswell No problem, frankly I'd long forgotten this issue.
Thanks for all your work on Matplotlib!

@github-actions
Copy link

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 28, 2023
@QuLogic
Copy link
Member

QuLogic commented Apr 28, 2023

The above linked PR should fix this, no need to close as inactive.

@QuLogic QuLogic removed the status: inactive Marked by the “Stale” Github Action label Apr 28, 2023
@QuLogic QuLogic modified the milestones: future releases, v3.8.0 Jun 13, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

7 participants