Skip to content
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

Support for Scalar Image Cursor Display? #3984

Closed
blink1073 opened this issue Jan 9, 2015 · 27 comments · Fixed by #3989
Closed

Support for Scalar Image Cursor Display? #3984

blink1073 opened this issue Jan 9, 2015 · 27 comments · Fixed by #3989
Milestone

Comments

@blink1073
Copy link
Member

How would everyone feel about the following change? It will show the z value of a scalar image in the cursor if available (similar to what Matlab does). If there is no image or the image is RGB, it will revert to the previous behaviour.

--- a/lib/matplotlib/axes/_base.py
+++ b/lib/matplotlib/axes/_base.py
@@ -2971,7 +2971,12 @@ class _AxesBase(martist.Artist):
             ys = '???'
         else:
             ys = self.format_ydata(y)
-        return 'x=%s y=%s' % (xs, ys)
+        try:
+            z = self.images[0].get_array()[int(y + 0.5), int(x + 0.5)]
+            zs = self.format_xdata(z)
+            return 'x=%s y=%s z=%s' % (xs, ys, zs)
+        except:
+            return 'x=%s y=%s' % (xs, ys)

     def minorticks_on(self):
         'Add autoscaling minor ticks to the axes.'
@blink1073
Copy link
Member Author

We could also add a fmt_zdata which falls back on format_xdata if None, so the user can directly change the z axis format:

--- a/lib/matplotlib/axes/_base.py
+++ b/lib/matplotlib/axes/_base.py
@@ -438,6 +438,7 @@ class _AxesBase(martist.Artist):
         # funcs used to format x and y - fall back on major formatters
         self.fmt_xdata = None
         self.fmt_ydata = None
+        self.fmt_zdata = None

         self.set_cursor_props((1, 'k'))  # set the cursor properties for axes

@@ -2971,7 +2972,13 @@ class _AxesBase(martist.Artist):
             ys = '???'
         else:
             ys = self.format_ydata(y)
-        return 'x=%s y=%s' % (xs, ys)
+        try:
+            z = self.images[0].get_array()[int(y + 0.5), int(x + 0.5)]
+            formatter = self.fmt_zdata or self.format_xdata
+            zs = formatter(z)
+            return 'x=%s y=%s z=%s' % (xs, ys, zs)
+        except:
+            return 'x=%s y=%s' % (xs, ys)

     def minorticks_on(self):

@tacaswell
Copy link
Member

You need to also take the extent of the image into account.

@WeatherGod
Copy link
Member

Yeah, it is a lot more complicated than that. I made an exa!mole demoing it
in my upcoming book, but it is incomplete in that it only recognizes the
last image added (doesn't take into account the zsort order).
On Jan 8, 2015 9:36 PM, "Thomas A Caswell" notifications@github.com wrote:

You need to also take the extent of the image into account.


Reply to this email directly or view it on GitHub
#3984 (comment)
.

@WeatherGod
Copy link
Member

I should note that my example was partly based off of the functionality
provided by the mpldatacursor package.
On Jan 8, 2015 9:41 PM, "Benjamin Root" ben.root@ou.edu wrote:

Yeah, it is a lot more complicated than that. I made an exa!mole demoing
it in my upcoming book, but it is incomplete in that it only recognizes the
last image added (doesn't take into account the zsort order).
On Jan 8, 2015 9:36 PM, "Thomas A Caswell" notifications@github.com
wrote:

You need to also take the extent of the image into account.


Reply to this email directly or view it on GitHub
#3984 (comment)
.

@blink1073
Copy link
Member Author

I hear this complaint from everyone I introduce to Python, so I'd really like to nail this down.

@blink1073
Copy link
Member Author

Hack so far based on mpldatacursor:

--- a/lib/matplotlib/axes/_base.py
+++ b/lib/matplotlib/axes/_base.py
@@ -438,6 +438,7 @@ class _AxesBase(martist.Artist):
         # funcs used to format x and y - fall back on major formatters
         self.fmt_xdata = None
         self.fmt_ydata = None
+        self.fmt_zdata = None

         self.set_cursor_props((1, 'k'))  # set the cursor properties for axes

@@ -2961,6 +2962,17 @@ class _AxesBase(martist.Artist):
             val = func(y)
             return val

+    def _get_z_coord(self, x, y):
+        im = self.images[0]
+        xmin, xmax, ymin, ymax = im.get_extent()
+        if im.origin == 'upper':
+            ymin, ymax = ymax, ymin
+        data_extent = mtransforms.Bbox([[ymin, xmin], [ymax, xmax]])
+        array_extent = mtransforms.Bbox([[0, 0], im.get_array().shape[:2]])
+        trans = mtransforms.BboxTransformFrom(data_extent) +\
+            mtransforms.BboxTransformTo(array_extent)
+        return trans.transform_point([y,x]).astype(int)
+
     def format_coord(self, x, y):
         """Return a format string formatting the *x*, *y* coord"""
         if x is None:
@@ -2971,7 +2983,14 @@ class _AxesBase(martist.Artist):
             ys = '???'
         else:
             ys = self.format_ydata(y)
-        return 'x=%s y=%s' % (xs, ys)
+        try:
+            i, j = self._get_z_coord(x, y)
+            z = self.images[0].get_array()[i, j]
+            formatter = self.fmt_zdata or self.format_xdata
+            zs = formatter(z)
+            return 'x=%s y=%s z=%s' % (xs, ys, zs)
+        except:
+            return 'x=%s y=%s' % (xs, ys)

     def minorticks_on(self):
         'Add autoscaling minor ticks to the axes.'

@blink1073
Copy link
Member Author

With zorder handling:

--- a/lib/matplotlib/axes/_base.py
+++ b/lib/matplotlib/axes/_base.py
@@ -438,6 +438,7 @@ class _AxesBase(martist.Artist):
         # funcs used to format x and y - fall back on major formatters
         self.fmt_xdata = None
         self.fmt_ydata = None
+        self.fmt_zdata = None

         self.set_cursor_props((1, 'k'))  # set the cursor properties for axes

@@ -2961,6 +2962,16 @@ class _AxesBase(martist.Artist):
             val = func(y)
             return val

+    def _get_z_coord(self, im, x, y):
+        xmin, xmax, ymin, ymax = im.get_extent()
+        if im.origin == 'upper':
+            ymin, ymax = ymax, ymin
+        data_extent = mtransforms.Bbox([[ymin, xmin], [ymax, xmax]])
+        array_extent = mtransforms.Bbox([[0, 0], im.get_array().shape[:2]])
+        trans = mtransforms.BboxTransformFrom(data_extent) +\
+            mtransforms.BboxTransformTo(array_extent)
+        return trans.transform_point([y,x]).astype(int)
+
     def format_coord(self, x, y):
         """Return a format string formatting the *x*, *y* coord"""
         if x is None:
@@ -2971,7 +2982,15 @@ class _AxesBase(martist.Artist):
             ys = '???'
         else:
             ys = self.format_ydata(y)
-        return 'x=%s y=%s' % (xs, ys)
+        try:
+            im = self.images[np.argmax([im.zorder for im in self.images])]
+            i, j = self._get_z_coord(im, x, y)
+            z = im.get_array()[i, j]
+            formatter = self.fmt_zdata or self.format_xdata
+            zs = formatter(z)
+            return 'x=%s y=%s z=%s' % (xs, ys, zs)
+        except:
+            return 'x=%s y=%s' % (xs, ys)

     def minorticks_on(self):
         'Add autoscaling minor ticks to the axes.'

@tacaswell
Copy link
Member

All artists have picker logic, you could probably hi-jack that to sort out which, if any of the you are in and then take the top one of that sub-set.

This may be why no one has done this yet (obnoxious demanding devs) ;)

@blink1073
Copy link
Member Author

So basically all of mpldatacursor then...

@blink1073
Copy link
Member Author

The problem I see is that we don't have an event, which is needed by the picking logic and is used extensively by mpldatacursor.

@tacaswell
Copy link
Member

We should have a mouse motion event (as that is what is updating the
formatter).

On Thu Jan 08 2015 at 10:14:31 PM Steven Silvester notifications@github.com
wrote:

The problem I see is that we don't have an event, which is needed by
the picking logic and is used extensively by mpldatacursor.


Reply to this email directly or view it on GitHub
#3984 (comment)
.

@blink1073
Copy link
Member Author

So we'd need to plug in here:

./backend_bases.py:2790: s = event.inaxes.format_coord(event.xdata, event.ydata)

@blink1073
Copy link
Member Author

Get the s message there and then get an optional add-on message based on the artist...

@blink1073
Copy link
Member Author

Shucks, there is no artist assigned to a motion_notify_event.

@tacaswell
Copy link
Member

This is some-what related to the discussion in #2986 in that we were talking about (iirc with out re-reading it in detail) a way to stack the message strings from multiple axes, not just the 'top' one of a twin{x,y} pair.

This should also be linked to #3146

@blink1073
Copy link
Member Author

Here's what I see:
We look through all of the artists on event.inaxes, checking contains. Then we grab the largest in zorder.

@blink1073
Copy link
Member Author

This should handle #3416, but all we know about is event.inaxes. Is there a way to detect if an axes has a twin? If so, we could use your suggested ax1: x.... | ax2: x... display and run this same algo on both axes.

@blink1073
Copy link
Member Author

@tacaswell, what zdata would you want about other artist types? The size of an element?

@blink1073
Copy link
Member Author

Here's what I've got so far:

--- a/lib/matplotlib/backend_bases.py
+++ b/lib/matplotlib/backend_bases.py
@@ -2791,6 +2791,13 @@ class NavigationToolbar2(object):
             except (ValueError, OverflowError):
                 pass
             else:
+                ax = event.inaxes
+                artists = ax.artists + ax.images + ax.lines
+                artists = [a for a in artists if a.contains(event)[0]]
+                if artists:
+                    artist = artists[np.argmax([a.zorder for a in artists])]
+                    print(artist)
+
                 if len(self.mode):
                     self.set_message('%s, %s' % (self.mode, s))
                 else:

@WeatherGod
Copy link
Member

You guys are just trying to obsolete my book before it comes out, aren't
you? Can't you wait until the 2nd edition? ;-)
On Jan 8, 2015 10:43 PM, "Steven Silvester" notifications@github.com
wrote:

@tacaswell https://github.com/tacaswell, what zdata would you want
about other artist types? The size of an element?


Reply to this email directly or view it on GitHub
#3984 (comment)
.

@blink1073
Copy link
Member Author

Here's a working example that handles just images:

--- a/lib/matplotlib/backend_bases.py
+++ b/lib/matplotlib/backend_bases.py
@@ -51,6 +51,7 @@ from matplotlib import get_backend
 from matplotlib._pylab_helpers import Gcf

 from matplotlib.transforms import Bbox, TransformedBbox, Affine2D
+import matplotlib.transforms as mtransforms

 import matplotlib.tight_bbox as tight_bbox
 import matplotlib.textpath as textpath
@@ -2781,6 +2782,26 @@ class NavigationToolbar2(object):

                 self._lastCursor = cursors.MOVE

+    def _get_z_data(self, event):
+        images = event.inaxes.images
+        images = [im for im in images if im.contains(event)[0]]
+        if not images:
+            return ''
+        im = images[np.argmax([i.zorder for i in images])]
+        xmin, xmax, ymin, ymax = im.get_extent()
+        if im.origin == 'upper':
+            ymin, ymax = ymax, ymin
+        data_extent = mtransforms.Bbox([[ymin, xmin], [ymax, xmax]])
+        array_extent = mtransforms.Bbox([[0, 0], im.get_array().shape[:2]])
+        trans = mtransforms.BboxTransformFrom(data_extent) +\
+            mtransforms.BboxTransformTo(array_extent)
+        i, j = trans.transform_point([event.ydata, event.xdata]).astype(int)
+        z = im.get_array()[i, j]
+        if z.size > 1:
+            # Override default numpy formatting for this specific case. Bad idea?
+            z = ', '.join('{:0.3g}'.format(item) for item in z)
+        return 'z=%s' % z
+
     def mouse_move(self, event):
         self._set_cursor(event)

@@ -2791,6 +2812,7 @@ class NavigationToolbar2(object):
             except (ValueError, OverflowError):
                 pass
             else:
+                s += self._get_z_data(event)
                 if len(self.mode):
                     self.set_message('%s, %s' % (self.mode, s))
                 else:

@tacaswell
Copy link
Member

@blink1073 The proper OO way would be to add a no-op function to Artist to and then let artists sub-classes process the motion event and return their addition to the message.

@blink1073
Copy link
Member Author

I sense a PR coming...

@blink1073
Copy link
Member Author

What if there are multiple images though? Don't we still need a zorder arbiter?

@tacaswell
Copy link
Member

Yeah, it should probably figure out which of the artists it is over and then only ask the top artist to add an entry to the message. This will also help arbitrate when we have multiple crossing lines or lines on top of images, etc.

@blink1073
Copy link
Member Author

I think we should still check for contains, then check zorder, then ask that artist for its additional message, no?

@blink1073
Copy link
Member Author

Ah, I see that is what you already said...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants