From 4af03f44b6be8dce3651d7a93a593e110998dbe1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Trygve=20Magnus=20R=C3=A6der?= Date: Mon, 26 May 2025 20:16:00 +0200 Subject: [PATCH 1/3] Update to docs with regards to colorbar and colorizer --- .../images_contours_and_fields/multi_image.py | 46 ++++++------------- .../users_explain/colors/colorbar_only.py | 37 ++++++++++----- lib/matplotlib/colorbar.py | 12 +++-- lib/matplotlib/figure.py | 9 ++-- 4 files changed, 51 insertions(+), 53 deletions(-) diff --git a/galleries/examples/images_contours_and_fields/multi_image.py b/galleries/examples/images_contours_and_fields/multi_image.py index 4e6f6cc54a79..9769dbf5219d 100644 --- a/galleries/examples/images_contours_and_fields/multi_image.py +++ b/galleries/examples/images_contours_and_fields/multi_image.py @@ -11,15 +11,16 @@ value *x* in the image). If we want one colorbar to be representative for multiple images, we have -to explicitly ensure consistent data coloring by using the same data -normalization for all the images. We ensure this by explicitly creating a -``norm`` object that we pass to all the image plotting methods. +to explicitly ensure consistent data coloring by using the same +data-to-color pipeline for all the images. We ensure this by explicitly +creating a `matplotlib.colorizer.Colorizer` object that we pass to all +the image plotting methods. """ import matplotlib.pyplot as plt import numpy as np -from matplotlib import colors +import matplotlib as mpl np.random.seed(19680801) @@ -31,12 +32,13 @@ fig, axs = plt.subplots(2, 2) fig.suptitle('Multiple images') -# create a single norm to be shared across all images -norm = colors.Normalize(vmin=np.min(datasets), vmax=np.max(datasets)) +# create a single norm and colorizer to be shared across all images +norm = mpl.colors.Normalize(vmin=np.min(datasets), vmax=np.max(datasets)) +colorizer = mpl.colorizer.Colorizer(norm=norm) images = [] for ax, data in zip(axs.flat, datasets): - images.append(ax.imshow(data, norm=norm)) + images.append(ax.imshow(data, colorizer=colorizer)) fig.colorbar(images[0], ax=axs, orientation='horizontal', fraction=.1) @@ -45,30 +47,10 @@ # %% # The colors are now kept consistent across all images when changing the # scaling, e.g. through zooming in the colorbar or via the "edit axis, -# curves and images parameters" GUI of the Qt backend. This is sufficient -# for most practical use cases. -# -# Advanced: Additionally sync the colormap -# ---------------------------------------- -# -# Sharing a common norm object guarantees synchronized scaling because scale -# changes modify the norm object in-place and thus propagate to all images -# that use this norm. This approach does not help with synchronizing colormaps -# because changing the colormap of an image (e.g. through the "edit axis, -# curves and images parameters" GUI of the Qt backend) results in the image -# referencing the new colormap object. Thus, the other images are not updated. -# -# To update the other images, sync the -# colormaps using the following code:: -# -# def sync_cmaps(changed_image): -# for im in images: -# if changed_image.get_cmap() != im.get_cmap(): -# im.set_cmap(changed_image.get_cmap()) -# -# for im in images: -# im.callbacks.connect('changed', sync_cmaps) -# +# curves and images parameters" GUI of the Qt backend. Additionally, +# if the colormap of the colorizer is changed, (e.g. through the "edit +# axis, curves and images parameters" GUI of the Qt backend) this change +# propagates to the other plots and the colorbar. # # .. admonition:: References # @@ -77,6 +59,6 @@ # # - `matplotlib.axes.Axes.imshow` / `matplotlib.pyplot.imshow` # - `matplotlib.figure.Figure.colorbar` / `matplotlib.pyplot.colorbar` +# - `matplotlib.colorizer.Colorizer` # - `matplotlib.colors.Normalize` -# - `matplotlib.cm.ScalarMappable.set_cmap` # - `matplotlib.cbook.CallbackRegistry.connect` diff --git a/galleries/users_explain/colors/colorbar_only.py b/galleries/users_explain/colors/colorbar_only.py index a3f1d62042f4..ee97e91162ae 100644 --- a/galleries/users_explain/colors/colorbar_only.py +++ b/galleries/users_explain/colors/colorbar_only.py @@ -8,10 +8,11 @@ This tutorial shows how to build and customize standalone colorbars, i.e. without an attached plot. -A `~.Figure.colorbar` needs a "mappable" (`matplotlib.cm.ScalarMappable`) -object (typically, an image) which indicates the colormap and the norm to be -used. In order to create a colorbar without an attached image, one can instead -use a `.ScalarMappable` with no associated data. +A `~.Figure.colorbar` needs a "mappable" (`matplotlib.colorizer.ColorizingArtist`) +object (typically, an image) which contains a colorizer +(`matplotlib.colorizer.Colorizer`) that holds the data-to-color pipeline (norm and +colormap). In order to create a colorbar without an attached image, one can instead +use a `.ColorizingArtist` with no associated data. """ import matplotlib.pyplot as plt @@ -23,9 +24,11 @@ # ------------------------- # Here, we create a basic continuous colorbar with ticks and labels. # -# The arguments to the `~.Figure.colorbar` call are the `.ScalarMappable` -# (constructed using the *norm* and *cmap* arguments), the axes where the -# colorbar should be drawn, and the colorbar's orientation. +# The arguments to the `~.Figure.colorbar` call are a `.ColorizingArtist`, +# the axes where the colorbar should be drawn, and the colorbar's orientation. +# To crate a `.ColorizingArtist` one must first make `.Colorizer` that holds the +# desired *norm* and *cmap*. +# # # For more information see the `~matplotlib.colorbar` API. @@ -33,7 +36,9 @@ norm = mpl.colors.Normalize(vmin=5, vmax=10) -fig.colorbar(mpl.cm.ScalarMappable(norm=norm, cmap="cool"), +colorizer = mpl.colorizer.Colorizer(norm=norm, cmap="cool") + +fig.colorbar(mpl.colorizer.ColorizingArtist(colorizer), cax=ax, orientation='horizontal', label='Some Units') # %% @@ -47,7 +52,9 @@ fig, ax = plt.subplots(layout='constrained') -fig.colorbar(mpl.cm.ScalarMappable(norm=mpl.colors.Normalize(0, 1), cmap='magma'), +colorizer = mpl.colorizer.Colorizer(norm=mpl.colors.Normalize(0, 1), cmap='magma') + +fig.colorbar(mpl.colorizer.ColorizingArtist(colorizer), ax=ax, orientation='vertical', label='a colorbar label') # %% @@ -65,7 +72,9 @@ bounds = [-1, 2, 5, 7, 12, 15] norm = mpl.colors.BoundaryNorm(bounds, cmap.N, extend='both') -fig.colorbar(mpl.cm.ScalarMappable(norm=norm, cmap="viridis"), +colorizer = mpl.colorizer.Colorizer(norm=norm, cmap='viridis') + +fig.colorbar(mpl.colorizer.ColorizingArtist(colorizer), cax=ax, orientation='horizontal', label="Discrete intervals with extend='both' keyword") @@ -94,8 +103,10 @@ bounds = [1, 2, 4, 7, 8] norm = mpl.colors.BoundaryNorm(bounds, cmap.N) +colorizer = mpl.colorizer.Colorizer(norm=norm, cmap=cmap) + fig.colorbar( - mpl.cm.ScalarMappable(cmap=cmap, norm=norm), + mpl.colorizer.ColorizingArtist(colorizer), cax=ax, orientation='horizontal', extend='both', spacing='proportional', @@ -116,8 +127,10 @@ bounds = [-1.0, -0.5, 0.0, 0.5, 1.0] norm = mpl.colors.BoundaryNorm(bounds, cmap.N) +colorizer = mpl.colorizer.Colorizer(norm=norm, cmap=cmap) + fig.colorbar( - mpl.cm.ScalarMappable(cmap=cmap, norm=norm), + mpl.colorizer.ColorizingArtist(colorizer), cax=ax, orientation='horizontal', extend='both', extendfrac='auto', spacing='uniform', diff --git a/lib/matplotlib/colorbar.py b/lib/matplotlib/colorbar.py index db33698c5514..19bdbe605d88 100644 --- a/lib/matplotlib/colorbar.py +++ b/lib/matplotlib/colorbar.py @@ -16,8 +16,9 @@ import numpy as np import matplotlib as mpl -from matplotlib import _api, cbook, collections, cm, colors, contour, ticker +from matplotlib import _api, cbook, collections, colors, contour, ticker import matplotlib.artist as martist +import matplotlib.colorizer as mcolorizer import matplotlib.patches as mpatches import matplotlib.path as mpath import matplotlib.spines as mspines @@ -199,12 +200,12 @@ class Colorbar: Draw a colorbar in an existing Axes. Typically, colorbars are created using `.Figure.colorbar` or - `.pyplot.colorbar` and associated with `.ScalarMappable`\s (such as an + `.pyplot.colorbar` and associated with `.ColorizingArtist`\s (such as an `.AxesImage` generated via `~.axes.Axes.imshow`). In order to draw a colorbar not associated with other elements in the figure, e.g. when showing a colormap by itself, one can create an empty - `.ScalarMappable`, or directly pass *cmap* and *norm* instead of *mappable* + `.ColorizingArtist`, or directly pass *cmap* and *norm* instead of *mappable* to `Colorbar`. Useful public methods are :meth:`set_label` and :meth:`add_lines`. @@ -244,7 +245,7 @@ def __init__( ax : `~matplotlib.axes.Axes` The `~.axes.Axes` instance in which the colorbar is drawn. - mappable : `.ScalarMappable` + mappable : `.ColorizingArtist` The mappable whose colormap and norm will be used. To show the colors versus index instead of on a 0-1 scale, set the @@ -288,7 +289,8 @@ def __init__( colorbar and at the right for a vertical. """ if mappable is None: - mappable = cm.ScalarMappable(norm=norm, cmap=cmap) + colorizer = mcolorizer.Colorizer(norm=norm, cmap=cmap) + mappable = mcolorizer.ColorizingArtist(colorizer) self.mappable = mappable cmap = mappable.cmap diff --git a/lib/matplotlib/figure.py b/lib/matplotlib/figure.py index bf4e2253324f..c15da7597acd 100644 --- a/lib/matplotlib/figure.py +++ b/lib/matplotlib/figure.py @@ -1200,17 +1200,18 @@ def colorbar( Parameters ---------- mappable - The `matplotlib.cm.ScalarMappable` (i.e., `.AxesImage`, + The `matplotlib.colorizer.ColorizingArtist` (i.e., `.AxesImage`, `.ContourSet`, etc.) described by this colorbar. This argument is mandatory for the `.Figure.colorbar` method but optional for the `.pyplot.colorbar` function, which sets the default to the current image. - Note that one can create a `.ScalarMappable` "on-the-fly" to - generate colorbars not attached to a previously drawn artist, e.g. + Note that one can create a `.colorizer.ColorizingArtist` "on-the-fly" + to generate colorbars not attached to a previously drawn artist, e.g. :: - fig.colorbar(cm.ScalarMappable(norm=norm, cmap=cmap), ax=ax) + cr = colorizer.Colorizer(norm=norm, cmap=cmap) + fig.colorbar(colorizer.ColorizingArtist(cr), ax=ax) cax : `~matplotlib.axes.Axes`, optional Axes into which the colorbar will be drawn. If `None`, then a new From 155ae8a496e6a3faad6a9d7b3c7f5aa2853532b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Trygve=20Magnus=20R=C3=A6der?= Date: Sun, 1 Jun 2025 14:13:44 +0200 Subject: [PATCH 2/3] Updates based on code review --- .../examples/images_contours_and_fields/multi_image.py | 10 +++++----- galleries/users_explain/colors/colorbar_only.py | 8 ++++---- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/galleries/examples/images_contours_and_fields/multi_image.py b/galleries/examples/images_contours_and_fields/multi_image.py index 9769dbf5219d..11be73f3a267 100644 --- a/galleries/examples/images_contours_and_fields/multi_image.py +++ b/galleries/examples/images_contours_and_fields/multi_image.py @@ -20,7 +20,8 @@ import matplotlib.pyplot as plt import numpy as np -import matplotlib as mpl +import matplotlib.colorizer as mcolorizer +import matplotlib.colors as mcolors np.random.seed(19680801) @@ -32,9 +33,9 @@ fig, axs = plt.subplots(2, 2) fig.suptitle('Multiple images') -# create a single norm and colorizer to be shared across all images -norm = mpl.colors.Normalize(vmin=np.min(datasets), vmax=np.max(datasets)) -colorizer = mpl.colorizer.Colorizer(norm=norm) +# create a colorizer with a predefined norm to be shared across all images +norm = mcolors.Normalize(vmin=np.min(datasets), vmax=np.max(datasets)) +colorizer = mcolorizer.Colorizer(norm=norm) images = [] for ax, data in zip(axs.flat, datasets): @@ -61,4 +62,3 @@ # - `matplotlib.figure.Figure.colorbar` / `matplotlib.pyplot.colorbar` # - `matplotlib.colorizer.Colorizer` # - `matplotlib.colors.Normalize` -# - `matplotlib.cbook.CallbackRegistry.connect` diff --git a/galleries/users_explain/colors/colorbar_only.py b/galleries/users_explain/colors/colorbar_only.py index ee97e91162ae..34de2bc78888 100644 --- a/galleries/users_explain/colors/colorbar_only.py +++ b/galleries/users_explain/colors/colorbar_only.py @@ -8,11 +8,11 @@ This tutorial shows how to build and customize standalone colorbars, i.e. without an attached plot. -A `~.Figure.colorbar` needs a "mappable" (`matplotlib.colorizer.ColorizingArtist`) -object (typically, an image) which contains a colorizer -(`matplotlib.colorizer.Colorizer`) that holds the data-to-color pipeline (norm and -colormap). In order to create a colorbar without an attached image, one can instead +A `~.Figure.colorbar` requires a `matplotlib.colorizer.ColorizingArtist` which +contains a `matplotlib.colorizer.Colorizer` that holds the data-to-color pipeline +(norm and colormap). To create a colorbar without an attached plot one can use a `.ColorizingArtist` with no associated data. + """ import matplotlib.pyplot as plt From 150165be075bded5f51f8d16e197711fb706d4bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Trygve=20Magnus=20R=C3=A6der?= Date: Mon, 2 Jun 2025 22:22:14 +0200 Subject: [PATCH 3/3] Tiny update from code review --- galleries/users_explain/colors/colorbar_only.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/galleries/users_explain/colors/colorbar_only.py b/galleries/users_explain/colors/colorbar_only.py index 34de2bc78888..b956fae43a1b 100644 --- a/galleries/users_explain/colors/colorbar_only.py +++ b/galleries/users_explain/colors/colorbar_only.py @@ -11,7 +11,8 @@ A `~.Figure.colorbar` requires a `matplotlib.colorizer.ColorizingArtist` which contains a `matplotlib.colorizer.Colorizer` that holds the data-to-color pipeline (norm and colormap). To create a colorbar without an attached plot one can -use a `.ColorizingArtist` with no associated data. +directly instantiate the base class `.ColorizingArtist`, which has no associated +data. """