Skip to content

FIX: add support for imshow extent to have units #22230

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

Merged
merged 1 commit into from
Nov 4, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions doc/users/next_whats_new/imshow_extent_units.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
The *extent* of ``imshow`` can now be expressed with units
----------------------------------------------------------
The *extent* parameter of `~.axes.Axes.imshow` and `~.AxesImage.set_extent`
can now be expressed with units.

.. plot::
:include-source: true

import matplotlib.pyplot as plt
import numpy as np
from matplotlib.dates import HourLocator, DateFormatter
from matplotlib.ticker import AutoMinorLocator

fig, ax = plt.subplots()
date_first = np.datetime64('2020-01-01', 'D')
date_last = np.datetime64('2020-01-11', 'D')

arr = [[i+j for i in range(10)] for j in range(10)]

ax.imshow(arr, origin='lower', extent=[1, 11, date_first, date_last])

ax.yaxis.set_major_formatter(DateFormatter('%d/%m/%y:- %H00hours'))
ax.yaxis.set_major_locator(HourLocator(byhour=[0, 6, 12, 18, 24]))
ax.yaxis.set_minor_locator(AutoMinorLocator())

plt.show()
1 change: 1 addition & 0 deletions lib/matplotlib/axes/_axes.py
Original file line number Diff line number Diff line change
Expand Up @@ -5555,6 +5555,7 @@ def imshow(self, X, cmap=None, norm=None, aspect=None,

extent : floats (left, right, bottom, top), optional
The bounding box in data coordinates that the image will fill.
These values may be unitful and match the units of the Axes.
The image is stretched individually along x and y to fill the box.

The default extent is determined by the following conditions.
Expand Down
27 changes: 25 additions & 2 deletions lib/matplotlib/image.py
Original file line number Diff line number Diff line change
Expand Up @@ -953,7 +953,7 @@ def _check_unsampled_image(self):
"""Return whether the image would be better drawn unsampled."""
return self.get_interpolation() == "none"

def set_extent(self, extent):
def set_extent(self, extent, **kwargs):
"""
Set the image extent.

Expand All @@ -962,6 +962,10 @@ def set_extent(self, extent):
extent : 4-tuple of float
The position and size of the image as tuple
``(left, right, bottom, top)`` in data coordinates.
**kwargs
Other parameters from which unit info (i.e., the *xunits*,
*yunits*, *zunits* (for 3D axes), *runits* and *thetaunits* (for
polar axes) entries are applied, if present.

Notes
-----
Expand All @@ -970,7 +974,26 @@ def set_extent(self, extent):
state is not changed, so following this with ``ax.autoscale_view()``
will redo the autoscaling in accord with ``dataLim``.
"""
self._extent = xmin, xmax, ymin, ymax = extent
(xmin, xmax), (ymin, ymax) = self.axes._process_unit_info(
[("x", [extent[0], extent[1]]),
("y", [extent[2], extent[3]])],
kwargs)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
kwargs)
kwargs)
if len(kwargs):
raise ValueError(f"set_extent did not consume all of the kwargs passed {list(kwargs)!r} were unused")

I _process_unit_info is very forgiving and is meant to be used in contexts where something else is going to make sure we do not have any extra kwargs. However in this case we are silently drop them on the floor (which in not great because now a typo will get silently ignored and not do what the wants....bugs where I am sure that xnuints is right but not doing what I want are among the most maddening!)

Because the keywords used depends on the type of axes we are sitting on (😱 ) I think that this is the best option?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

made the changes

Copy link
Contributor Author

@pratimugale pratimugale Oct 11, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The checks have passed, could you please have a look?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see any check for leftover kwargs?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had added a check for the leftover kwargs. Could you please let me know what is supposed to be done when there are kwargs remaining? For eg, if we raise a ValueError as before, then other test cases break - like when cmap is used as a kwarg

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You will need to be more explicit about what gets passed to set_extent from __init__, so that you can raise in the manner noted above.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done. Does imshow even need to pass kwargs to set_extents? I have removed it. So that the user can pass those keyword arguments by calling set_extents and not in imshow. Does that sound correct?

if len(kwargs):
raise ValueError(
"set_extent did not consume all of the kwargs passed." +
f"{list(kwargs)!r} were unused"
)
xmin = self.axes._validate_converted_limits(
xmin, self.convert_xunits)
xmax = self.axes._validate_converted_limits(
xmax, self.convert_xunits)
ymin = self.axes._validate_converted_limits(
ymin, self.convert_yunits)
ymax = self.axes._validate_converted_limits(
ymax, self.convert_yunits)
extent = [xmin, xmax, ymin, ymax]

self._extent = extent
corners = (xmin, ymin), (xmax, ymax)
self.axes.update_datalim(corners)
self.sticky_edges.x[:] = [xmin, xmax]
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
36 changes: 36 additions & 0 deletions lib/matplotlib/tests/test_axes.py
Original file line number Diff line number Diff line change
Expand Up @@ -8272,3 +8272,39 @@ def test_bar_all_nan(fig_test, fig_ref):

ax_ref.bar([1], [1]).remove()
ax_ref.bar([1], [1])


@image_comparison(["extent_units.png"], style="mpl20")
def test_extent_units():
_, axs = plt.subplots(2, 2)
date_first = np.datetime64('2020-01-01', 'D')
date_last = np.datetime64('2020-01-11', 'D')
arr = [[i+j for i in range(10)] for j in range(10)]

axs[0, 0].set_title('Date extents on y axis')
im = axs[0, 0].imshow(arr, origin='lower',
extent=[1, 11, date_first, date_last],
cmap=mpl.colormaps["plasma"])

axs[0, 1].set_title('Date extents on x axis (Day of Jan 2020)')
im = axs[0, 1].imshow(arr, origin='lower',
extent=[date_first, date_last, 1, 11],
cmap=mpl.colormaps["plasma"])
axs[0, 1].xaxis.set_major_formatter(mdates.DateFormatter('%d'))

im = axs[1, 0].imshow(arr, origin='lower',
extent=[date_first, date_last,
date_first, date_last],
cmap=mpl.colormaps["plasma"])
axs[1, 0].xaxis.set_major_formatter(mdates.DateFormatter('%d'))
axs[1, 0].set(xlabel='Day of Jan 2020')

im = axs[1, 1].imshow(arr, origin='lower',
cmap=mpl.colormaps["plasma"])
im.set_extent([date_last, date_first, date_last, date_first])
axs[1, 1].xaxis.set_major_formatter(mdates.DateFormatter('%d'))
axs[1, 1].set(xlabel='Day of Jan 2020')

with pytest.raises(ValueError,
match="set_extent did not consume all of the kwargs"):
im.set_extent([2, 12, date_first, date_last], clip=False)