5
votes

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()
2
The commented out code you have also seems to work so I am confused what the problem is. - tacaswell
@tcaswell: I simplified my code too much. When I use ax2.semilogy, for some reason the commented-out code raises an IndexError. - unutbu
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
I think I found a work-around. - tacaswell

2 Answers

3
votes

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.

1
votes

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.