From e1a8fee6e600fb5bbd96e03303cfb8614d4cfbb9 Mon Sep 17 00:00:00 2001 From: David Stansby Date: Wed, 24 Jan 2024 17:05:30 +0000 Subject: [PATCH 01/13] Basic support for units on ScalarMappable --- .../next_api_changes/behavior/27721-DS.rst | 16 +++++++++ doc/users/next_whats_new/image_units.rst | 8 +++++ lib/matplotlib/cm.py | 34 ++++++++++++++++++- lib/matplotlib/image.py | 4 +++ lib/matplotlib/tests/test_units.py | 30 ++++++++++++++++ lib/matplotlib/units.py | 10 ++++-- 6 files changed, 99 insertions(+), 3 deletions(-) create mode 100644 doc/api/next_api_changes/behavior/27721-DS.rst create mode 100644 doc/users/next_whats_new/image_units.rst diff --git a/doc/api/next_api_changes/behavior/27721-DS.rst b/doc/api/next_api_changes/behavior/27721-DS.rst new file mode 100644 index 000000000000..3fa3740c49d7 --- /dev/null +++ b/doc/api/next_api_changes/behavior/27721-DS.rst @@ -0,0 +1,16 @@ +Unit converters can now support units in images +----------------------------------------------- + +`~.cm.ScalarMappable` can now contain data with units. This adds support for +unit-ful data to be plotted using - `~.axes.Axes.imshow`, `~.axes.Axes.pcolor`, +and `~.axes.Axes.pcolormesh` + +For this to be supported by third-party `~.units.ConversionInterface`, +the `~.units.ConversionInterface.default_units` and +`~.units.ConversionInterface.convert` methods must allow for the *axis* +argument to be ``None``, and `~.units.ConversionInterface.convert` must be able to +convert data of more than one dimension (e.g. when plotting images the data is 2D). + +If a conversion interface raises an error when given ``None`` or 2D data as described +above, this error will be re-raised when a user tries to use one of the newly supported +plotting methods with unit-ful data. diff --git a/doc/users/next_whats_new/image_units.rst b/doc/users/next_whats_new/image_units.rst new file mode 100644 index 000000000000..25117cdce37b --- /dev/null +++ b/doc/users/next_whats_new/image_units.rst @@ -0,0 +1,8 @@ +Unit support for images +----------------------- +This adds support for image data with units has been added to the following plotting +methods: + +- `~.axes.Axes.imshow` +- `~.axes.Axes.pcolor` +- `~.axes.Axes.pcolormesh` diff --git a/lib/matplotlib/cm.py b/lib/matplotlib/cm.py index c14973560ac3..6f693c5247b3 100644 --- a/lib/matplotlib/cm.py +++ b/lib/matplotlib/cm.py @@ -24,6 +24,7 @@ from matplotlib import _api, colors, cbook, scale from matplotlib._cm import datad from matplotlib._cm_listed import cmaps as cmaps_listed +import matplotlib.units as munits _LUTSIZE = mpl.rcParams['image.lut'] @@ -283,6 +284,8 @@ def __init__(self, norm=None, cmap=None): The colormap used to map normalized data values to RGBA colors. """ self._A = None + self._units = None + self._converter = None self._norm = None # So that the setter knows we're initializing. self.set_norm(norm) # The Normalize instance of this ScalarMappable. self.cmap = None # So that the setter knows we're initializing. @@ -393,6 +396,35 @@ def to_rgba(self, x, alpha=None, bytes=False, norm=True): rgba = self.cmap(x, alpha=alpha, bytes=bytes) return rgba + def _strip_units(self, A): + """ + Remove units from A, and save the units and converter used to do the conversion. + """ + self._converter = munits.registry.get_converter(A) + if self._converter is None: + self._units = None + return A + + try: + self._units = self._converter.default_units(A, None) + except Exception as e: + raise RuntimeError( + f'{self._converter} failed when trying to return the default units for ' + 'this image. This may be because support has not been ' + 'implemented for `axis=None` in the default_units() method.' + ) from e + + try: + A = self._converter.convert(A, self._units, None) + except Exception as e: + raise munits.ConversionError( + f'{self._converter} failed when trying to convert the units for this ' + 'image. This may be because support has not been implemented ' + 'for `axis=None` in the convert() method.' + ) from e + + return A + def set_array(self, A): """ Set the value array from array-like *A*. @@ -408,7 +440,7 @@ def set_array(self, A): if A is None: self._A = None return - + A = self._strip_units(A) A = cbook.safe_masked_invalid(A, copy=True) if not np.can_cast(A.dtype, float, "same_kind"): raise TypeError(f"Image data of dtype {A.dtype} cannot be " diff --git a/lib/matplotlib/image.py b/lib/matplotlib/image.py index 73738fe3bdbe..294c38951d18 100644 --- a/lib/matplotlib/image.py +++ b/lib/matplotlib/image.py @@ -726,6 +726,8 @@ def set_data(self, A): """ if isinstance(A, PIL.Image.Image): A = pil_to_array(A) # Needed e.g. to apply png palette. + + A = self._strip_units(A) self._A = self._normalize_image_array(A) self._imcache = None self.stale = True @@ -1140,6 +1142,7 @@ def set_data(self, x, y, A): (M, N) `~numpy.ndarray` or masked array of values to be colormapped, or (M, N, 3) RGB array, or (M, N, 4) RGBA array. """ + A = self._strip_units(A) A = self._normalize_image_array(A) x = np.array(x, np.float32) y = np.array(y, np.float32) @@ -1300,6 +1303,7 @@ def set_data(self, x, y, A): - (M, N, 3): RGB array - (M, N, 4): RGBA array """ + A = self._strip_units(A) A = self._normalize_image_array(A) x = np.arange(0., A.shape[1] + 1) if x is None else np.array(x, float).ravel() y = np.arange(0., A.shape[0] + 1) if y is None else np.array(y, float).ravel() diff --git a/lib/matplotlib/tests/test_units.py b/lib/matplotlib/tests/test_units.py index a5fd32dfb3e5..b6c57d22db7f 100644 --- a/lib/matplotlib/tests/test_units.py +++ b/lib/matplotlib/tests/test_units.py @@ -41,6 +41,9 @@ def __getitem__(self, item): def __array__(self): return np.asarray(self.magnitude) + def __len__(self): + return len(self.__array__()) + @pytest.fixture def quantity_converter(): @@ -302,3 +305,30 @@ def test_plot_kernel(): # just a smoketest that fail kernel = Kernel([1, 2, 3, 4, 5]) plt.plot(kernel) + + +@image_comparison(['mappable_units.png'], style="mpl20") +def test_mappable_units(quantity_converter): + # Check that showing an image with units works + munits.registry[Quantity] = quantity_converter + x, y = np.meshgrid([0, 1], [0, 1]) + data = Quantity(np.arange(4).reshape(2, 2), 'hours') + + fig, axs = plt.subplots(nrows=2, ncols=2) + + # imshow + ax = axs[0, 0] + mappable = ax.imshow(data, origin='lower') + cbar = fig.colorbar(mappable, ax=ax) + + # pcolor + ax = axs[0, 1] + mappable = ax.pcolor(x, y, data) + fig.colorbar(mappable, ax=ax) + + # pcolormesh + horizontal colorbar + ax = axs[1, 0] + mappable = ax.pcolormesh(x, y, data) + fig.colorbar(mappable, ax=ax, orientation="horizontal") + + axs[1, 1].axis("off") diff --git a/lib/matplotlib/units.py b/lib/matplotlib/units.py index e3480f228bb4..1d15c038a444 100644 --- a/lib/matplotlib/units.py +++ b/lib/matplotlib/units.py @@ -118,16 +118,22 @@ def axisinfo(unit, axis): @staticmethod def default_units(x, axis): - """Return the default unit for *x* or ``None`` for the given axis.""" + """ + Return the default unit for *x*. + + *axis* may be an `~.axis.Axis` or ``None``. + """ return None @staticmethod def convert(obj, unit, axis): """ - Convert *obj* using *unit* for the specified *axis*. + Convert *obj* using *unit*. If *obj* is a sequence, return the converted sequence. The output must be a sequence of scalars that can be used by the numpy array layer. + + *axis* may be an `~.axis.Axis` or ``None``. """ return obj From 0ccb72b48047fbe967f363f00d79cd5570c3c047 Mon Sep 17 00:00:00 2001 From: David Stansby Date: Wed, 24 Jan 2024 18:11:22 +0000 Subject: [PATCH 02/13] Set units on colorbar axes --- lib/matplotlib/colorbar.py | 28 ++++++++++++++++++++++++++-- lib/matplotlib/colorbar.pyi | 2 +- 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/lib/matplotlib/colorbar.py b/lib/matplotlib/colorbar.py index af61e4671ff4..91c49710784d 100644 --- a/lib/matplotlib/colorbar.py +++ b/lib/matplotlib/colorbar.py @@ -291,7 +291,7 @@ def __init__(self, ax, mappable=None, *, cmap=None, drawedges=False, extendfrac=None, extendrect=False, - label='', + label=None, location=None, ): @@ -370,6 +370,7 @@ def __init__(self, ax, mappable=None, *, cmap=None, self.solids_patches = [] self.lines = [] + for spine in self.ax.spines.values(): spine.set_visible(False) self.outline = self.ax.spines['outline'] = _ColorbarSpine(self.ax) @@ -391,8 +392,10 @@ def __init__(self, ax, mappable=None, *, cmap=None, orientation) if location is None else location self.ticklocation = ticklocation - self.set_label(label) + if label is not None: + self.set_label(label) self._reset_locator_formatter_scale() + self._set_units_from_mappable() if np.iterable(ticks): self._locator = ticker.FixedLocator(ticks, nbins=len(ticks)) @@ -1331,6 +1334,27 @@ def drag_pan(self, button, key, x, y): elif self.orientation == 'vertical': self.norm.vmin, self.norm.vmax = points[:, 1] + def _set_units_from_mappable(self): + """ + Set the colorbar locator and formatter if the mappable has units. + """ + self._units = self.mappable._units + self._converter = self.mappable._converter + if self._converter is not None: + + axis = self._long_axis() + info = self._converter.axisinfo(self._units, axis) + + if info is not None: + if info.majloc is not None: + self.locator = info.majloc + if info.minloc is not None: + self.minorlocator = info.minloc + if info.majfmt is not None: + self.formatter = info.majfmt + if info.minfmt is not None: + self.minorformatter = info.minfmt + ColorbarBase = Colorbar # Backcompat API diff --git a/lib/matplotlib/colorbar.pyi b/lib/matplotlib/colorbar.pyi index f71c5759fc55..b065214ff445 100644 --- a/lib/matplotlib/colorbar.pyi +++ b/lib/matplotlib/colorbar.pyi @@ -59,7 +59,7 @@ class Colorbar: drawedges: bool = ..., extendfrac: Literal["auto"] | float | Sequence[float] | None = ..., extendrect: bool = ..., - label: str = ..., + label: str | None = ..., location: Literal["left", "right", "top", "bottom"] | None = ... ) -> None: ... @property From f2fa21b675d1f8dfa959f49a417e8e26fe4aa475 Mon Sep 17 00:00:00 2001 From: David Stansby Date: Thu, 25 Jan 2024 08:54:17 +0000 Subject: [PATCH 03/13] Add unit conversion for pcolor methods --- lib/matplotlib/axes/_axes.py | 39 ++++++++++++++++++++++++++++-------- 1 file changed, 31 insertions(+), 8 deletions(-) diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index cc310b7da0d7..2bfc79630983 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -8,6 +8,7 @@ from numpy import ma import matplotlib as mpl +import matplotlib.cm as cm import matplotlib.category # Register category unit converter as side effect. import matplotlib.cbook as cbook import matplotlib.collections as mcoll @@ -5838,12 +5839,28 @@ def imshow(self, X, cmap=None, norm=None, *, aspect=None, self.add_image(im) return im + @staticmethod + def _convert_C_units(C): + """ + Remove any units attached to C, and return the units and converter used to do + the conversion. + """ + sm = cm.ScalarMappable() + C = sm._strip_units(C) + converter = sm._converter + units = sm._units + + C = np.asanyarray(C) + C = cbook.safe_masked_invalid(C, copy=True) + return C, units, converter + def _pcolorargs(self, funcname, *args, shading='auto', **kwargs): # - create X and Y if not present; # - reshape X and Y as needed if they are 1-D; # - check for proper sizes based on `shading` kwarg; # - reset shading if shading='auto' to flat or nearest # depending on size; + # - if C has units, get the converter _valid_shading = ['gouraud', 'nearest', 'flat', 'auto'] try: @@ -5855,7 +5872,7 @@ def _pcolorargs(self, funcname, *args, shading='auto', **kwargs): shading = 'auto' if len(args) == 1: - C = np.asanyarray(args[0]) + C, units, converter = self._convert_C_units(args[0]) nrows, ncols = C.shape[:2] if shading in ['gouraud', 'nearest']: X, Y = np.meshgrid(np.arange(ncols), np.arange(nrows)) @@ -5863,11 +5880,11 @@ def _pcolorargs(self, funcname, *args, shading='auto', **kwargs): X, Y = np.meshgrid(np.arange(ncols + 1), np.arange(nrows + 1)) shading = 'flat' C = cbook.safe_masked_invalid(C, copy=True) - return X, Y, C, shading + return X, Y, C, shading, units, converter if len(args) == 3: # Check x and y for bad data... - C = np.asanyarray(args[2]) + C, units, converter = self._convert_C_units(args[2]) # unit conversion allows e.g. datetime objects as axis values X, Y = args[:2] X, Y = self._process_unit_info([("x", X), ("y", Y)], kwargs) @@ -5948,7 +5965,7 @@ def _interp_grid(X): shading = 'flat' C = cbook.safe_masked_invalid(C, copy=True) - return X, Y, C, shading + return X, Y, C, shading, units, converter @_preprocess_data() @_docstring.dedent_interpd @@ -6100,8 +6117,9 @@ def pcolor(self, *args, shading=None, alpha=None, norm=None, cmap=None, if shading is None: shading = mpl.rcParams['pcolor.shading'] shading = shading.lower() - X, Y, C, shading = self._pcolorargs('pcolor', *args, shading=shading, - kwargs=kwargs) + X, Y, C, shading, units, converter = self._pcolorargs( + 'pcolor', *args, shading=shading, kwargs=kwargs + ) linewidths = (0.25,) if 'linewidth' in kwargs: kwargs['linewidths'] = kwargs.pop('linewidth') @@ -6137,6 +6155,8 @@ def pcolor(self, *args, shading=None, alpha=None, norm=None, cmap=None, collection = mcoll.PolyQuadMesh( coords, array=C, cmap=cmap, norm=norm, alpha=alpha, **kwargs) + collection._units = units + collection._converter = converter collection._scale_norm(norm, vmin, vmax) # Transform from native to data coordinates? @@ -6356,8 +6376,9 @@ def pcolormesh(self, *args, alpha=None, norm=None, cmap=None, vmin=None, shading = shading.lower() kwargs.setdefault('edgecolors', 'none') - X, Y, C, shading = self._pcolorargs('pcolormesh', *args, - shading=shading, kwargs=kwargs) + X, Y, C, shading, units, converter = self._pcolorargs( + 'pcolormesh', *args, shading=shading, kwargs=kwargs + ) coords = np.stack([X, Y], axis=-1) kwargs.setdefault('snap', mpl.rcParams['pcolormesh.snap']) @@ -6365,6 +6386,8 @@ def pcolormesh(self, *args, alpha=None, norm=None, cmap=None, vmin=None, collection = mcoll.QuadMesh( coords, antialiased=antialiased, shading=shading, array=C, cmap=cmap, norm=norm, alpha=alpha, **kwargs) + collection._units = units + collection._converter = converter collection._scale_norm(norm, vmin, vmax) coords = coords.reshape(-1, 2) # flatten the grid structure; keep x, y From 4a7504d56efae0e703c79ea7c876081b7eea10ca Mon Sep 17 00:00:00 2001 From: David Stansby Date: Tue, 30 Jan 2024 10:49:31 +0000 Subject: [PATCH 04/13] Add support for units in vmin/vmax --- lib/matplotlib/cm.py | 12 ++++++++---- lib/matplotlib/tests/test_units.py | 16 +++++++++------- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/lib/matplotlib/cm.py b/lib/matplotlib/cm.py index 6f693c5247b3..f0b7d2270178 100644 --- a/lib/matplotlib/cm.py +++ b/lib/matplotlib/cm.py @@ -490,10 +490,14 @@ def set_clim(self, vmin=None, vmax=None): vmin, vmax = vmin except (TypeError, ValueError): pass - if vmin is not None: - self.norm.vmin = colors._sanitize_extrema(vmin) - if vmax is not None: - self.norm.vmax = colors._sanitize_extrema(vmax) + + def _process_lim(lim): + if self._converter is not None: + lim = self._converter.convert(lim, self._units, axis=None) + return colors._sanitize_extrema(lim) + + self.norm.vmin = _process_lim(vmin) + self.norm.vmax = _process_lim(vmax) def get_alpha(self): """ diff --git a/lib/matplotlib/tests/test_units.py b/lib/matplotlib/tests/test_units.py index b6c57d22db7f..73ecdf67f723 100644 --- a/lib/matplotlib/tests/test_units.py +++ b/lib/matplotlib/tests/test_units.py @@ -313,22 +313,24 @@ def test_mappable_units(quantity_converter): munits.registry[Quantity] = quantity_converter x, y = np.meshgrid([0, 1], [0, 1]) data = Quantity(np.arange(4).reshape(2, 2), 'hours') + vmin = Quantity(1, "hours") # Test a limit different from min of the data + vmax = Quantity(3 * 60, "minutes") # Test a different unit to the data - fig, axs = plt.subplots(nrows=2, ncols=2) + fig, axs = plt.subplots(nrows=2, ncols=2, constrained_layout=True) # imshow ax = axs[0, 0] - mappable = ax.imshow(data, origin='lower') - cbar = fig.colorbar(mappable, ax=ax) + mappable = ax.imshow(data, origin='lower', vmin=vmin, vmax=vmax) + cbar = fig.colorbar(mappable, ax=ax, extend="min") # pcolor ax = axs[0, 1] - mappable = ax.pcolor(x, y, data) - fig.colorbar(mappable, ax=ax) + mappable = ax.pcolor(x, y, data, vmin=vmin, vmax=vmax) + fig.colorbar(mappable, ax=ax, extend="min") # pcolormesh + horizontal colorbar ax = axs[1, 0] - mappable = ax.pcolormesh(x, y, data) - fig.colorbar(mappable, ax=ax, orientation="horizontal") + mappable = ax.pcolormesh(x, y, data, vmin=vmin, vmax=vmax) + fig.colorbar(mappable, ax=ax, orientation="horizontal", extend="min") axs[1, 1].axis("off") From 794b4ed366866d531d771b69e4483d7e4afa7a5a Mon Sep 17 00:00:00 2001 From: David Stansby Date: Tue, 30 Jan 2024 15:07:24 +0000 Subject: [PATCH 05/13] Allow unit registry to look up 2D-like input --- lib/matplotlib/units.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/matplotlib/units.py b/lib/matplotlib/units.py index 1d15c038a444..f886eaefe342 100644 --- a/lib/matplotlib/units.py +++ b/lib/matplotlib/units.py @@ -192,7 +192,7 @@ def get_converter(self, x): else: # ... and avoid infinite recursion for pathological iterables for # which indexing returns instances of the same iterable class. - if type(first) is not type(x): + if isinstance(first, list) or type(first) is not type(x): return self.get_converter(first) return None From 084850bfaed75ca9c97a199b1e82113b08f410ba Mon Sep 17 00:00:00 2001 From: David Stansby Date: Tue, 30 Jan 2024 15:07:42 +0000 Subject: [PATCH 06/13] Add a datetime test to images with units --- lib/matplotlib/tests/test_units.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/matplotlib/tests/test_units.py b/lib/matplotlib/tests/test_units.py index 73ecdf67f723..0fe7b4d9edd6 100644 --- a/lib/matplotlib/tests/test_units.py +++ b/lib/matplotlib/tests/test_units.py @@ -324,6 +324,11 @@ def test_mappable_units(quantity_converter): cbar = fig.colorbar(mappable, ax=ax, extend="min") # pcolor + # Use datetime to check that the locator/formatter is set correctly + data = [[datetime(2024, 1, 1), datetime(2024, 1, 2)], + [datetime(2024, 1, 3), datetime(2024, 1, 4)]] + vmin = datetime(2024, 1, 2) + vmax = datetime(2024, 1, 5) ax = axs[0, 1] mappable = ax.pcolor(x, y, data, vmin=vmin, vmax=vmax) fig.colorbar(mappable, ax=ax, extend="min") From 979b099ed59fdc60a452e12a08afe939510029ef Mon Sep 17 00:00:00 2001 From: David Stansby Date: Tue, 30 Jan 2024 16:58:22 +0000 Subject: [PATCH 07/13] Add support for categorical colorbars --- doc/api/next_api_changes/behavior/27721-DS.rst | 4 ++++ lib/matplotlib/category.py | 4 +++- lib/matplotlib/cm.py | 6 ++++++ lib/matplotlib/tests/test_units.py | 5 +++-- 4 files changed, 16 insertions(+), 3 deletions(-) diff --git a/doc/api/next_api_changes/behavior/27721-DS.rst b/doc/api/next_api_changes/behavior/27721-DS.rst index 3fa3740c49d7..b451ea30dc58 100644 --- a/doc/api/next_api_changes/behavior/27721-DS.rst +++ b/doc/api/next_api_changes/behavior/27721-DS.rst @@ -14,3 +14,7 @@ convert data of more than one dimension (e.g. when plotting images the data is 2 If a conversion interface raises an error when given ``None`` or 2D data as described above, this error will be re-raised when a user tries to use one of the newly supported plotting methods with unit-ful data. + +If you have a custom conversion interface you want to forbid using with image data, the +`~.units.ConversionInterface` methods that accept a ``units`` parameter should raise +a `matplotlib.units.ConversionError` when given ``units=None``. diff --git a/lib/matplotlib/category.py b/lib/matplotlib/category.py index 4ac2379ea5f5..4492770a84e1 100644 --- a/lib/matplotlib/category.py +++ b/lib/matplotlib/category.py @@ -101,6 +101,8 @@ def default_units(data, axis): object storing string to integer mapping """ # the conversion call stack is default_units -> axis_info -> convert + if axis is None: + return UnitData(data) if axis.units is None: axis.set_units(UnitData(data)) else: @@ -208,7 +210,7 @@ def update(self, data): TypeError If elements in *data* are neither str nor bytes. """ - data = np.atleast_1d(np.array(data, dtype=object)) + data = np.atleast_1d(np.array(data, dtype=object).ravel()) # check if convertible to number: convertible = True for val in OrderedDict.fromkeys(data): diff --git a/lib/matplotlib/cm.py b/lib/matplotlib/cm.py index f0b7d2270178..b9b18dc28594 100644 --- a/lib/matplotlib/cm.py +++ b/lib/matplotlib/cm.py @@ -408,6 +408,9 @@ def _strip_units(self, A): try: self._units = self._converter.default_units(A, None) except Exception as e: + if isinstance(e, munits.ConversionError): + raise e + raise RuntimeError( f'{self._converter} failed when trying to return the default units for ' 'this image. This may be because support has not been ' @@ -417,6 +420,9 @@ def _strip_units(self, A): try: A = self._converter.convert(A, self._units, None) except Exception as e: + if isinstance(e, munits.ConversionError): + raise e + raise munits.ConversionError( f'{self._converter} failed when trying to convert the units for this ' 'image. This may be because support has not been implemented ' diff --git a/lib/matplotlib/tests/test_units.py b/lib/matplotlib/tests/test_units.py index 0fe7b4d9edd6..20eebf29af36 100644 --- a/lib/matplotlib/tests/test_units.py +++ b/lib/matplotlib/tests/test_units.py @@ -333,9 +333,10 @@ def test_mappable_units(quantity_converter): mappable = ax.pcolor(x, y, data, vmin=vmin, vmax=vmax) fig.colorbar(mappable, ax=ax, extend="min") - # pcolormesh + horizontal colorbar + # pcolormesh + horizontal colorbar + categorical + data = [["one", "two"], ["three", "four"]] ax = axs[1, 0] - mappable = ax.pcolormesh(x, y, data, vmin=vmin, vmax=vmax) + mappable = ax.pcolormesh(x, y, data) fig.colorbar(mappable, ax=ax, orientation="horizontal", extend="min") axs[1, 1].axis("off") From 6832e0f24e2dae999370cef1e4cacb4a306da1c4 Mon Sep 17 00:00:00 2001 From: David Stansby Date: Tue, 30 Jan 2024 17:14:36 +0000 Subject: [PATCH 08/13] Allow ticks argument to take units --- lib/matplotlib/colorbar.py | 6 ++++-- lib/matplotlib/tests/test_units.py | 6 +++++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/lib/matplotlib/colorbar.py b/lib/matplotlib/colorbar.py index 91c49710784d..dfa046656191 100644 --- a/lib/matplotlib/colorbar.py +++ b/lib/matplotlib/colorbar.py @@ -370,7 +370,6 @@ def __init__(self, ax, mappable=None, *, cmap=None, self.solids_patches = [] self.lines = [] - for spine in self.ax.spines.values(): spine.set_visible(False) self.outline = self.ax.spines['outline'] = _ColorbarSpine(self.ax) @@ -397,9 +396,12 @@ def __init__(self, ax, mappable=None, *, cmap=None, self._reset_locator_formatter_scale() self._set_units_from_mappable() + if ticks is not None and self._converter is not None: + ticks = self._converter.convert(ticks, self._units, self._long_axis()) + if np.iterable(ticks): self._locator = ticker.FixedLocator(ticks, nbins=len(ticks)) - else: + elif isinstance(ticks, ticker.Locator): self._locator = ticks if isinstance(format, str): diff --git a/lib/matplotlib/tests/test_units.py b/lib/matplotlib/tests/test_units.py index 20eebf29af36..a9c771d3a6dd 100644 --- a/lib/matplotlib/tests/test_units.py +++ b/lib/matplotlib/tests/test_units.py @@ -324,14 +324,18 @@ def test_mappable_units(quantity_converter): cbar = fig.colorbar(mappable, ax=ax, extend="min") # pcolor + # # Use datetime to check that the locator/formatter is set correctly + # Also test ticks argument to colorbar data = [[datetime(2024, 1, 1), datetime(2024, 1, 2)], [datetime(2024, 1, 3), datetime(2024, 1, 4)]] vmin = datetime(2024, 1, 2) vmax = datetime(2024, 1, 5) ax = axs[0, 1] mappable = ax.pcolor(x, y, data, vmin=vmin, vmax=vmax) - fig.colorbar(mappable, ax=ax, extend="min") + + ticks = [datetime(2024, 1, 2), datetime(2024, 1, 3), datetime(2024, 1, 5)] + fig.colorbar(mappable, ax=ax, extend="min", ticks=ticks) # pcolormesh + horizontal colorbar + categorical data = [["one", "two"], ["three", "four"]] From 8bad4fb8af8476bb2d8aabf66b063d1c6652e456 Mon Sep 17 00:00:00 2001 From: David Stansby Date: Tue, 30 Jan 2024 17:19:17 +0000 Subject: [PATCH 09/13] Note where else units can be used with images --- doc/users/next_whats_new/image_units.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/doc/users/next_whats_new/image_units.rst b/doc/users/next_whats_new/image_units.rst index 25117cdce37b..bcaad73ded4c 100644 --- a/doc/users/next_whats_new/image_units.rst +++ b/doc/users/next_whats_new/image_units.rst @@ -6,3 +6,7 @@ methods: - `~.axes.Axes.imshow` - `~.axes.Axes.pcolor` - `~.axes.Axes.pcolormesh` + +If the data has units, the ``vmin`` and ``vmax`` units to these methods can also have +units, and if you add a colorbar the ``levels`` argument to ``colorbar`` can also +have units. From 4a7caac2ec8a4e1b18b6da3f1846ebdbfc50ebe4 Mon Sep 17 00:00:00 2001 From: David Stansby Date: Tue, 30 Jan 2024 18:24:09 +0000 Subject: [PATCH 10/13] Update collections test --- lib/matplotlib/tests/test_collections.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/matplotlib/tests/test_collections.py b/lib/matplotlib/tests/test_collections.py index 5baaeaa5d388..aa666ad790a3 100644 --- a/lib/matplotlib/tests/test_collections.py +++ b/lib/matplotlib/tests/test_collections.py @@ -794,7 +794,7 @@ def test_collection_set_array(): # Test set_array with wrong dtype with pytest.raises(TypeError, match="^Image data of dtype"): - c.set_array("wrong_input") + c.set_array(object()) # Test if array kwarg is copied vals[5] = 45 From 0a17be969b87bfd2e2b7b2cc729e10dc6924ad63 Mon Sep 17 00:00:00 2001 From: David Stansby Date: Tue, 30 Jan 2024 21:24:41 +0000 Subject: [PATCH 11/13] Add test figure --- .../test_units/mappable_units.png | Bin 0 -> 28430 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 lib/matplotlib/tests/baseline_images/test_units/mappable_units.png diff --git a/lib/matplotlib/tests/baseline_images/test_units/mappable_units.png b/lib/matplotlib/tests/baseline_images/test_units/mappable_units.png new file mode 100644 index 0000000000000000000000000000000000000000..4d1896e9189beb11da305b31442723e06bff38f5 GIT binary patch literal 28430 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 JLFD(-zX3}c#{&QW literal 0 HcmV?d00001 From ad272c56e162ac794d4b06a3b60b9272630a227e Mon Sep 17 00:00:00 2001 From: David Stansby Date: Tue, 30 Jan 2024 21:31:03 +0000 Subject: [PATCH 12/13] Coerce array in pickle tests --- lib/matplotlib/tests/test_pickle.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/matplotlib/tests/test_pickle.py b/lib/matplotlib/tests/test_pickle.py index cab412bff561..549bab79ba51 100644 --- a/lib/matplotlib/tests/test_pickle.py +++ b/lib/matplotlib/tests/test_pickle.py @@ -111,7 +111,7 @@ def test_complete(fig_test, fig_ref): loaded.canvas.draw() fig_test.set_size_inches(loaded.get_size_inches()) - fig_test.figimage(loaded.canvas.renderer.buffer_rgba()) + fig_test.figimage(np.asarray(loaded.canvas.renderer.buffer_rgba())) plt.close(loaded) @@ -151,7 +151,7 @@ def test_pickle_load_from_subprocess(fig_test, fig_ref, tmp_path): loaded_fig.canvas.draw() fig_test.set_size_inches(loaded_fig.get_size_inches()) - fig_test.figimage(loaded_fig.canvas.renderer.buffer_rgba()) + fig_test.figimage(np.asarray(loaded_fig.canvas.renderer.buffer_rgba())) plt.close(loaded_fig) From 03405380ba2077026f11850e4686c1084b4d3f2f Mon Sep 17 00:00:00 2001 From: David Stansby Date: Tue, 30 Jan 2024 22:44:52 +0000 Subject: [PATCH 13/13] Change EllipseCollection private attribute name --- lib/matplotlib/collections.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/lib/matplotlib/collections.py b/lib/matplotlib/collections.py index fb137cc503e1..0796b926f8f1 100644 --- a/lib/matplotlib/collections.py +++ b/lib/matplotlib/collections.py @@ -1751,7 +1751,7 @@ def __init__(self, widths, heights, angles, *, units='points', **kwargs): self._widths = 0.5 * np.asarray(widths).ravel() self._heights = 0.5 * np.asarray(heights).ravel() self._angles = np.deg2rad(angles).ravel() - self._units = units + self._length_units = units self.set_transform(transforms.IdentityTransform()) self._transforms = np.empty((0, 3, 3)) self._paths = [mpath.Path.unit_circle()] @@ -1762,24 +1762,24 @@ def _set_transforms(self): ax = self.axes fig = self.figure - if self._units == 'xy': + if self._length_units == 'xy': sc = 1 - elif self._units == 'x': + elif self._length_units == 'x': sc = ax.bbox.width / ax.viewLim.width - elif self._units == 'y': + elif self._length_units == 'y': sc = ax.bbox.height / ax.viewLim.height - elif self._units == 'inches': + elif self._length_units == 'inches': sc = fig.dpi - elif self._units == 'points': + elif self._length_units == 'points': sc = fig.dpi / 72.0 - elif self._units == 'width': + elif self._length_units == 'width': sc = ax.bbox.width - elif self._units == 'height': + elif self._length_units == 'height': sc = ax.bbox.height - elif self._units == 'dots': + elif self._length_units == 'dots': sc = 1.0 else: - raise ValueError(f'Unrecognized units: {self._units!r}') + raise ValueError(f'Unrecognized units: {self._length_units!r}') self._transforms = np.zeros((len(self._widths), 3, 3)) widths = self._widths * sc @@ -1793,7 +1793,7 @@ def _set_transforms(self): self._transforms[:, 2, 2] = 1.0 _affine = transforms.Affine2D - if self._units == 'xy': + if self._length_units == 'xy': m = ax.transData.get_affine().get_matrix().copy() m[:2, 2:] = 0 self.set_transform(_affine(m))