6

I would like to track the coordinates of the mouse with respect to data coordinates on two axes simultaneously. I can track the mouse position with respect to one axis just fine. The problem is: when I add a second axis with twinx(), both Cursors report data coordinates with respect to the second axis only.

For example, my Cursors (fern and muffy) report the y-value is 7.93

Fern: (1597.63, 7.93)
Muffy: (1597.63, 7.93)

If I use:

    inv = ax.transData.inverted()
    x, y = inv.transform((event.x, event.y))

I get an IndexError.

So the question is: How can I modify the code to track the data coordinates with respect to both axes?


enter image description here

import numpy as np
import matplotlib.pyplot as plt
import logging
logger = logging.getLogger(__name__)

class Cursor(object):
    def __init__(self, ax, name):
        self.ax = ax
        self.name = name
        plt.connect('motion_notify_event', self)

    def __call__(self, event):
        x, y = event.xdata, event.ydata
        ax = self.ax
        # inv = ax.transData.inverted()
        # x, y = inv.transform((event.x, event.y))
        logger.debug('{n}: ({x:0.2f}, {y:0.2f})'.format(n=self.name,x=x,y=y))


logging.basicConfig(level=logging.DEBUG,
                    format='%(message)s',)
fig, ax = plt.subplots()

x = np.linspace(1000, 2000, 500)
y = 100*np.sin(20*np.pi*(x-1500)/2000.0)
fern = Cursor(ax, 'Fern')
ax.plot(x,y)
ax2 = ax.twinx()
z = x/200.0
muffy = Cursor(ax2, 'Muffy')
ax2.semilogy(x,z)
plt.show()
unutbu
  • 842,883
  • 184
  • 1,785
  • 1,677
  • The commented out code you have also seems to work so I am confused what the problem is. – tacaswell May 21 '13 at 14:52
  • @tcaswell: I simplified my code too much. When I use `ax2.semilogy`, for some reason the commented-out code raises an IndexError. – unutbu May 21 '13 at 15:02
  • I think this is a bug, if you get the inverted transform for the `transData` it does not behave properly outside of the call back and is raising errors from `transfroms.py` – tacaswell May 21 '13 at 15:21
  • I think I found a work-around. – tacaswell May 21 '13 at 15:32

2 Answers2

5

Due to the way that the call backs work, the event always returns in the top axes. You just need a bit of logic to check which if the event happens in the axes we want:

class Cursor(object):
    def __init__(self, ax, x, y, name):
        self.ax = ax
        self.name = name
        plt.connect('motion_notify_event', self)

    def __call__(self, event):
        if event.inaxes is None:
            return
        ax = self.ax
        if ax != event.inaxes:
            inv = ax.transData.inverted()
            x, y = inv.transform(np.array((event.x, event.y)).reshape(1, 2)).ravel()
        elif ax == event.inaxes:
            x, y = event.xdata, event.ydata
        else:
            return
        logger.debug('{n}: ({x:0.2f}, {y:0.2f})'.format(n=self.name,x=x,y=y))

This might be a subtle bug down in the transform stack (or this is the correct usage and it was by luck it worked with tuples before), but at any rate, this will make it work. The issue is that the code at line 1996 in transform.py expects to get a 2D ndarray back, but the identity transform just returns the tuple that get handed into it, which is what generates the errors.

tacaswell
  • 84,579
  • 22
  • 210
  • 199
  • @unutbu yes, but one could argue there is no point in re-doing the display -> data conversion that has already been done to populate `xdata` and `ydata` attributes – tacaswell May 21 '13 at 15:42
  • This has been really helpful. Thanks so much. – unutbu May 21 '13 at 15:48
  • someone did and I would like to know why (guessed you as I left this link on a question you commented on). No worries, sorry for the noise. – tacaswell Feb 05 '14 at 20:05
2

You can track both axis coordinates with one cursor (or event handler) this way:

import numpy as np
import matplotlib.pyplot as plt
import logging
logger = logging.getLogger(__name__)

class Cursor(object):
    def __init__(self):
        plt.connect('motion_notify_event', self)

    def __call__(self, event):
        if event.inaxes is None:
            return
        x, y1 = ax1.transData.inverted().transform((event.x,event.y))
        x, y2 = ax2.transData.inverted().transform((event.x,event.y))
        logger.debug('(x,y1,y2)=({x:0.2f}, {y1:0.2f}, {y2:0.2f})'.format(x=x,y1=y1,y2=y2))

logging.basicConfig(level=logging.DEBUG,
                    format='%(message)s',)
fig, ax1 = plt.subplots()

x = np.linspace(1000, 2000, 500)
y = 100*np.sin(20*np.pi*(x-1500)/2000.0)
fern = Cursor()
ax1.plot(x,y)
ax2 = ax1.twinx()
z = x/200.0
ax2.plot(x,z)
plt.show()

(I got "too many indices" when I used ax2.semilogy(x,z) like the OP, but didn't work through that problem.)

The ax1.transData.inverted().transform((event.x,event.y)) code performs a transform from display to data coordinates on the specified axis and can be used with either axis at will.

CarpeCimex
  • 135
  • 2
  • 10
  • Thanks @CarpeCimex. The problem is, I use the `Cursor` in situations where there is only one axes as well as with twinx axes. So I don't want Cursor to assume there is always two axes, `ax1` and `ax2`. – unutbu Nov 01 '14 at 23:26
  • I see. You can put `x, y = ax.transData.inverted().transform((event.x,event.y))` in your original `__call__` function in place of the commented-out code and do just that, though. (There is still the "too many indices" problem when using `ax2.semilog()` though, but that's a separate problem.) – CarpeCimex Nov 02 '14 at 00:21
  • Oh... I see now, this question does not make a whole to sense any more since the bug in matplotlib has now been fixed. – unutbu Nov 02 '14 at 00:59
  • @CarpeCimex Thank you for your answer. I have a simpler situation (always `ax` and `ax.twinx()` overlapped) and this helped a lot! – Guimoute May 01 '20 at 09:44