Skip to content

Multivar imshow - #30597

Merged
timhoffm merged 13 commits into
matplotlib:mainfrom
trygvrad:multivar_imshow
Jul 27, 2026
Merged

Multivar imshow#30597
timhoffm merged 13 commits into
matplotlib:mainfrom
trygvrad:multivar_imshow

Conversation

@trygvrad

@trygvrad trygvrad commented Sep 24, 2025

Copy link
Copy Markdown
Contributor

Exposes the functionality of MultiNorm, BivarColormap and MultivarColormap to the top level plotting functions ax.imshow(), ax.pcolor() and ax.pcolormesh(). This closes #30526, see Bivariate and Multivariate Colormapping
As a side-effect of the pcolor/pcolormesh implementation, Collection also gets the new functionality.

In short, this PR allows you to plot multivariate data more easily, but it does not:

  • Create equivalents to fig.colorbar() for BivarColormap and MultivarColormap to work with ColorizingArtist
  • Select bivariate and multivariate colormaps to include in matplotlib
  • Examples demonstrating the new functionality

These will come in later PRs. See Bivariate and Multivariate Colormapping

Examples demonstrating new functionality:

import numpy as np
import matplotlib as mpl
import matplotlib.pyplot as plt
cmap = mpl.bivar_colormaps['BiPeak']
x_0 = np.arange(25, dtype='float32').reshape(5, 5) % 5
x_1 = np.arange(25, dtype='float32').reshape(5, 5).T % 5
x_0, x_1 = x_0 + 0.3*x_1, x_0*-0.3 + x_1, 

fig, axes = plt.subplots(1, 3, figsize=(6, 2))
axes[0].imshow(x_0, cmap=cmap[0])
axes[1].imshow(x_1, cmap=cmap[1])
axes[2].imshow((x_0, x_1), cmap=cmap)
axes[0].set_title('data 0')
axes[1].set_title('data 1')
axes[2].set_title('data 0 and 1')
image
fig, axes = plt.subplots(1, 6, figsize=(10, 2.3))
axes[0].imshow((x_0, x_1), cmap='BiPeak', interpolation='nearest')
axes[1].matshow((x_0, x_1), cmap='BiPeak')
axes[2].pcolor((x_0, x_1), cmap='BiPeak')
axes[3].pcolormesh((x_0, x_1), cmap='BiPeak')

x = np.arange(5)
y = np.arange(5)
X, Y = np.meshgrid(x, y)
axes[4].pcolormesh(X, Y, (x_0, x_1), cmap='BiPeak')

patches = [
    mpl.patches.Wedge((.3, .7), .1, 0, 360),             # Full circle
    mpl.patches.Wedge((.7, .8), .2, 0, 360, width=0.05),  # Full ring
    mpl.patches.Wedge((.8, .3), .2, 0, 45),              # Full sector
    mpl.patches.Wedge((.8, .3), .2, 22.5, 90, width=0.10),  # Ring sector
]
colors_0 = np.arange(len(patches)) // 2
colors_1 = np.arange(len(patches)) % 2
p = mpl.collections.PatchCollection(patches, cmap='BiPeak', alpha=0.5)
p.set_array((colors_0, colors_1))
axes[5].add_collection(p)
axes[0].set_title('imshow')
axes[1].set_title('matshow')
axes[2].set_title('pcolor')
axes[3].set_title('pcolormesh (C)')
axes[4].set_title('pcolormesh (X, Y, C)')
axes[5].set_title('PatchCollection')
fig.tight_layout()
image

@trygvrad

trygvrad commented Nov 2, 2025

Copy link
Copy Markdown
Contributor Author

I fixed the circleci doc error for this.
It would be great if someone could take a look :)
@QuLogic @story645 @ksunden @timhoffm

Comment thread lib/matplotlib/cbook.py
Comment thread lib/matplotlib/axes/_axes.py Outdated
Comment thread lib/matplotlib/axes/_axes.py
Comment thread lib/matplotlib/axes/_axes.py Outdated
Comment thread lib/matplotlib/axes/_axes.py
fig, axes = plt.subplots(2, 3)

# interpolation='nearest' to reduce size of baseline image
axes[0, 0].imshow(x_1, interpolation='nearest', alpha=0.5)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

are the other interpolations tested?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Nope!,
I'm changing one of tests so that it is :)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

feeling silly but can't find the test with this change

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

It's in the following test test_multivariate_visualizations()

line 10101 does imshow without specifying interpolation: axes[0].imshow((x_0, x_1, x_2), cmap='3VarAddA')
multivariate_visualizations

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

doesn't imshow usually default to nearest though? https://matplotlib.org/devdocs/api/_as_gen/matplotlib.axes.Axes.imshow.html#matplotlib-axes-axes-imshow

Like what happens if interpolation is set to none?

Comment thread lib/matplotlib/image.py Outdated
Comment thread lib/matplotlib/image.py Outdated
Comment thread lib/matplotlib/image.py Outdated
Comment thread lib/matplotlib/image.py Outdated

@ksunden ksunden left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

General thoughts on return types:

Doing things like float | tuple[float, ...] as is done for several things here (vmin/vmax, clip, etc) is potentially problematic.

Humans may easily work with that, but type checkers will likely yell that they didn't check for all possible outcomes

None is a bit of a special case in being more acceptable (easier to check, etc)

Consider moving these in new code to always return a tuple (even if single element) This keeps the branching needed to a minimum and is not too cumbersome to work for in the single variable case.

Obviously, existing APIs need to maintain back-compat, so this is limited to new code.

Consider whether conceptually an empty tuple is what is truly meant by the None case, but if it is not, retain None

Comment thread lib/matplotlib/axes/_axes.pyi Outdated
Comment thread lib/matplotlib/axes/_axes.pyi Outdated
Comment thread lib/matplotlib/axes/_axes.pyi Outdated
Comment thread lib/matplotlib/colorizer.pyi Outdated
Comment thread lib/matplotlib/pyplot.py Outdated
Comment thread lib/matplotlib/pyplot.py Outdated
Comment thread lib/matplotlib/pyplot.py Outdated
@trygvrad

trygvrad commented Dec 1, 2025

Copy link
Copy Markdown
Contributor Author

General thoughts on return types:
Doing things like float | tuple[float, ...] as is done for several things here (vmin/vmax, clip, etc) is potentially problematic.
Humans may easily work with that, but type checkers will likely yell that they didn't check for all possible outcomes

We discussed change the behaviour of colorizer to always return tuples on the call last week.

The relevant moving parts here are:

  1. Norm (Normalize, MultiNorm): members: vmin, vmax, clip
  2. Colorizer: members: get/set_clim, get/set_clip, vmin, vmax, clip
  3. _ColorizingInterface: members: get/set_clim, get/set_clip

The Norm ABC must be typed as follows for backwards compatibility:
def vmin(self) -> float | tuple[float | None, ...] | None: ...


For the Colorizer, I think it makes sense to force tuples on the getter, but allow both on the setter:

    def get_clim(self) -> tuple[tuple[float | None, ...], tuple[float | None, ...]]: ...
    def set_clim(self, vmin: float | tuple[float, ...] | None = ..., vmax: float | tuple[float, ...] | None = ...) -> None: ...

For the _ColorizingInterface we have two options.
A: allow both
def set_clim(self, vmin: float | tuple[float, float] | tuple[float | None, ...] | None, vmax: float | tuple[float | None, ...] | None = ...) -> None: ...
B: Allow get/set_clim only when using scalar data, and otherwise encourage the user to use the colorizer interface:
def set_clim(self, vmin: float | tuple[float, float] | None = ..., vmax: float | None = ...) -> None: ...

    def get_clim(self):
        """
        Return the values (min, max) that are mapped to the colormap limits.

        This function is not available for multivariate data.
        """
        if self._colorizer.norm.n_components > 1:
            raise AttributeError("`.get_clim()` is unavailable when using a colormap "
                                 "with multiple components. Use "
                                 "`.colorizer.get_clim()` instead.")
        return self.colorizer.norm.vmin, self.colorizer.norm.vmax

One reason why I favor option B, is that set_clim is already sufficiently complicated, because for scalar data it allows both signatures:
.set_clim(vmin=vmin, vmax=vmax)
.set_clim((vmin, vmax))
and the 2nd option is ambiguous if there are two colors

@ksunden @story645 Could you let me know what you think?

@story645 story645 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Sorry for the very long delay in reviewing. Minor nits but I think this is fine otherwise.

Comment thread lib/matplotlib/axes/_axes.py Outdated
Comment thread lib/matplotlib/axes/_axes.py
fig, axes = plt.subplots(2, 3)

# interpolation='nearest' to reduce size of baseline image
axes[0, 0].imshow(x_1, interpolation='nearest', alpha=0.5)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

feeling silly but can't find the test with this change

@trygvrad

trygvrad commented Jun 7, 2026

Copy link
Copy Markdown
Contributor Author

@timhoffm @ksunden
As discussed at the meeting on Thursday, I have now rebased this PR.
Once this is merged, we can more easily review #31214

@timhoffm timhoffm left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Mostly small changes that I noticed when re-reading the PR. Let's still get them in. Please add them as an additional commit for easier review. We'll squash-merge in the end.

Comment thread lib/matplotlib/axes/_axes.py Outdated
Comment thread lib/matplotlib/axes/_axes.py
Comment thread lib/matplotlib/axes/_axes.py Outdated
Comment on lines +6736 to +6737
C = mcolorizer._ensure_multivariate_data(args[-1],
colorizer.cmap.n_variates)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I feel _ensure_multivariate_data is not a good name because:

  • "ensure" has more connotation of validation, not necessarily conversion
  • this does not necessarily output multivariate data.

Good naming is hard and I propose to do this as a follow-up as this is internal and has been here before the PR.

Comment thread lib/matplotlib/colorizer.py Outdated
Comment thread lib/matplotlib/colorizer.py Outdated
Comment thread lib/matplotlib/image.py
@trygvrad

Copy link
Copy Markdown
Contributor Author

@timhoffm
I am putting my replies here so that I can resolve the comments above without hiding the answers for future reference.

What about structured arrays, are they supported too? Do we possibly need one place to define "multivariate data" that can be referenced?

Yes we support structured data as well. On a related note the internal _ImageBase class requires structured data, and as requested I updated the docstring of this so that it reads:

            - a (M, N) array interpreted as scalar (greyscale) image,
              with one of the dtypes `~numpy.float32`, `~numpy.float64`,
              `~numpy.float128`, `~numpy.uint16` or `~numpy.uint8`.
            - a (M, N) structured array with K fields for multivariate colormapping.
              This must be used with a `.BivarColormap` (K=2) or generally with a
              K-component `.MultivarColormap`.
            - (M, N, 4) RGBA image with a dtype of `~numpy.float32`,
              `~numpy.float64`, `~numpy.float128`, or `~numpy.uint8`.

How should we add the option of structured data to the top level functions?
Should we include the two ways to include multivariate data on one line, i.e. something like this?:

            - a (K, M, N) scalar array or a structured (M, N) array with K fields:
              a K-component M*N mesh for multivariate colormapping. This must be 
              used with a `.BivarColormap` (K=2) or generally with a K-component 
              `.MultivarColormap`.

Also, did we discuss (K, M, N) vs. (M, N, K)? (Sorry in case I bring up topics that we may have discussed before)

Yes, this has been discussed, and it this keeps coming back up. I believe the primary discussions on this was at the weekly meeting around this time. I know I have made multiple posts on this before, but I have a difficult time finding them among all the different PRs.
If we want to discuss this again I suggest we bring it up at a weekly meeting and make sure that this time we write some things in the meeting notes :)

Also, it states "This parameter is ignored if X is RGB(A)."

I'm changing this to Scalar colormaps are ignored if *X* is RGB(A). which better reflects the current behaviour both on main and in this pr.

plt.imshow(np.random.random((6, 7, 3), cmap='not_a_colormap')

will cause an exception on main, while only if the cmap argument is a valid scalar colormap is the paramater ignored, i.e.:

plt.imshow(np.random.random((6, 7, 3), cmap='viridis')

How do we know RGB(A), i.e. shape (M, N, 3) or (M, N, 4) if there is (K, N, M) as multivariate data. Is this now a heuristic that the first or last dimension is low?

The multivariate pipeline is triggered by a valid multivariate colormap, thus we have:

i.e.:

plt.imshow(np.random.random((3, 3, 3))                        →   interpreted as rgb image
plt.imshow(np.random.random((3, 3, 3), cmap='viridis')        →   interpreted as rgb image
plt.imshow(np.random.random((3, 3, 3), cmap='not_a_cmap')     →   raises an error
plt.imshow(np.random.random((3, 3, 3), cmap='3VarAddA')       →   uses a multivariate colormap

@trygvrad

Copy link
Copy Markdown
Contributor Author

@timhoffm A follow up on this would be much appreciated :)

@trygvrad

Copy link
Copy Markdown
Contributor Author

Mostly small changes that I noticed when re-reading the PR. Let's still get them in. Please add them as an additional commit for easier review. We'll squash-merge in the end.

@timhoffm I really need you to finish this review/approve this so that we can get it merged and other people can start reviewing #31214

@timhoffm

timhoffm commented Jul 24, 2026

Copy link
Copy Markdown
Member

Also, did we discuss (K, M, N) vs. (M, N, K)? (Sorry in case I bring up topics that we may have discussed before)

Yes, this has been discussed, and it this keeps coming back up. I believe the primary discussions on this was at the weekly meeting around #28428 (comment) time. I know I have made multiple posts on this before, but I have a difficult time finding them among all the different PRs.
If we want to discuss this again I suggest we bring it up at a weekly meeting and make sure that this time we write some things in the meeting notes :)

Of the top of my head, I see the primary motivation in that (K, M, N) is the array shape of a list of component arrays: [component_1, ..., component_K]. Though that's not too compelling. There is precedence for handling "list of datasets" differently that 2D array of datasets, e.g. in boxplot() or in grouped_bar.

For RGB, the shape would be (M, N, 3), and I'm inclined to draw an analogy from the color channels to components. This would speak for (M, N, K).

But the more important argument would be: What is the typical data structure people already have when they are working with multivariate data? - And I'm completely blank here. Do you have insights?

The multivariate pipeline is triggered by a valid multivariate colormap, thus we have:

i.e.:

plt.imshow(np.random.random((3, 3, 3))                        →   interpreted as rgb image
plt.imshow(np.random.random((3, 3, 3), cmap='viridis')        →   interpreted as rgb image
plt.imshow(np.random.random((3, 3, 3), cmap='not_a_cmap')     →   raises an error
plt.imshow(np.random.random((3, 3, 3), cmap='3VarAddA')       →   uses a multivariate colormap

This also means, if you have multivariate data, you must always specify a colormap. Is this documented explicitly? If not please add it because that's important to know.

It's probably ok to request this as there would need to be different multivar colormaps depending on the data. OTOH it may be nice to be able to drop some multivar data and matplotlib figures out a nice visualization. This could be added in the future; i.e. infer multivar or not primarily from the data structure; in case of ambiguit, e.g. (3, 3, 3), check the colormap, and if none was given, fall back to a scalar interpretation. This would be a straight forward extension of the current logic.

@timhoffm

Copy link
Copy Markdown
Member

Should we include the two ways to include multivariate data on one line, i.e. something like this?:

            - a (K, M, N) scalar array or a structured (M, N) array with K fields:
              a K-component M*N mesh for multivariate colormapping. This must be 
              used with a `.BivarColormap` (K=2) or generally with a K-component 
              `.MultivarColormap`.

Yes.

@trygvrad

Copy link
Copy Markdown
Contributor Author

@timhoffm Thank you for coming back to this :)

This also means, if you have multivariate data, you must always specify a colormap. Is this documented explicitly? If not please add it because that's important to know.

This is a good point. I was thinking the docstring could get this across, the I do not think I was sufficiently clear with the language. This should be better:

            - a (K, M, N) scalar array or a structured (M, N) array with K fields:
              a K-component M*N mesh for multivariate colormapping. A valid
              `.BivarColormap` (K=2) or K-component `.MultivarColormap` must be
              specified using the *cmap* keyword argument.

NOTE: I also change the section that describes the array in the docstring of pcolor so that it matches the docstring of pcolormesh. Both support RGB image data, but this was undocumented in the docstring for pcolor.

We have the option to detect when the user might be trying to use this feature in colors._ensure_multivariate_data(), where we could raise an error with a hint, for example if the user attempts to input data of shape (k, n, m) with 2 <= k < 10 and n,m >10 without the required colormap. Currently such data will raise an error, but the error raised by 'imshow' is different from the error raised by 'pcolor', 'pcolormesh'. We don't want to change these errors, as there are other top level functions that can invoke those errors that do not support multivariate color mapping (yet and perhaps never). I think we can get the feature in first, and then see if we want to update the errors later.


But the more important argument would be: What is the typical data structure people already have when they are working with multivariate data? - And I'm completely blank here. Do you have insights?

I can mostly speak from my own experience, and I have had use for this feature in two contexts:

  1. I am plotting results from two separate experiments together – i.e. the data is loaded from two different sources.
  2. I am plotting the results from two separate data processing pipelines acting on the same dataset.

In both cases, I end up with two separate handles, data_A, data_B, and the call signature ax.imshow((data_A, data_B), ...) is the most convenient.

(I started implementing multivariate color mapping in matplotlib because I used to work with dark-field X-ray microscopy which typically produces 4D datasets that reduce to a series of 2D datasets through moment analysis (intensity, center of mass in x[orientation], center of mass in theta [strain], 2nd moments, ...), and we needed 2D colormaps to visualize them. Note that we would often plot things that are qualitatively very different together, such as orientation[mrad] and strain[dimensionless], in order to look for correlations. We would use different computational pipelines to get to the different moments, and represent them with different variable names.)

I have limited experience with fluorescence microscopy, but I think it is worth noting that the typical format OMETIFF stores data as (T, C, Z, Y, X), where T is typically time, and C is typically the color [fluorophore? wavelength?]. Both T, C, and Z follow before Y and X because the microscope always only reads 2D images sequentially, and this becomes the natural way to store the data as it comes out of the instruement.

The only argument I have seen for (N, M, K), is the analogy to RGB images, however to me this is a very weak argument, as the two approaches are trying to achieve very different things. You should not visualize data from an RGB camera using multivariate color mapping, and likewise, you should not map some arbitrary data space to RGB – doing so is likely to cause you to misinterpret your data. With this in mind, have different call signatures for the two approaches is to me a feature to help avoid mistakes.

Comment thread lib/matplotlib/axes/_axes.py Outdated
Comment on lines +6241 to +6244
- a (K, M, N) scalar array or a structured (M, N) array with K fields:
a K-component M*N mesh for multivariate colormapping. A valid
`.BivarColormap` (K=2) or K-component `.MultivarColormap` must be
specified using the *cmap* keyword argument.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
- a (K, M, N) scalar array or a structured (M, N) array with K fields:
a K-component M*N mesh for multivariate colormapping. A valid
`.BivarColormap` (K=2) or K-component `.MultivarColormap` must be
specified using the *cmap* keyword argument.
- (K, M, N) scalar array
- structured (M, N) array with K fields
- K-component M*N mesh
A valid `.BivarColormap` (K=2) or K-component `.MultivarColormap` must be
specified using the *cmap* keyword argument.

Wondering if something like this might be slightly cleaner cause I'm unclear about the various allowed k dimensional inputs.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I think you are reading the first two lines as three options, rather than two, and if you are reading it this way, it means the current text is not sufficiently clear. Let me see if I can reformulate it in a less confusing way.

@story645 how about this?

            - a (K, M, N) scalar array or a structured (M, N) array with K fields.
              The K channels are mapped to colors using a `.MultiNorm` and a 
              `.BivarColormap` (K=2) or K-component `.MultivarColormap`.
              This input option is only available when a `.BivarColormap` or 
              `.MultivarColormap` is provided to the *cmap* keyword argument.

This formulation follows the same structure as the bullet point above to make it easier to parse:

            - (M, N) or M*N: a mesh with scalar data. The values are mapped to
              colors using normalization and a colormap. See parameters *norm*,
              *cmap*, *vmin*, *vmax*.

I would prefer to keep everything regarding multivariate color mapping behind one bullet, so that readers who do not want this can easily skip the entire bulletpoint.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Much better, thanks!

@timhoffm timhoffm left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The argument for (K, M, N) over (M, N, K) sounds reasonabe.

From my side, this is good to go!

@story645

story645 commented Jul 26, 2026

Copy link
Copy Markdown
Member

This needs to pass PR cleanliness but otherwise looks good to me too. @trygve are you waiting on input from anybody else?

@trygvrad

Copy link
Copy Markdown
Contributor Author

This needs to pass PR cleanliness but otherwise looks good to me too. @trygvrad are you waiting on input from anybody else?

I think this can be squash merged now, and that should take care of the cleanliness :)

Comment thread lib/matplotlib/axes/_axes.py Outdated
@timhoffm
timhoffm merged commit ca156cf into matplotlib:main Jul 27, 2026
26 of 32 checks passed
@github-project-automation github-project-automation Bot moved this from Ready to be merged to Done in Bivariate and Multivariate Colormapping Jul 27, 2026
@timhoffm

Copy link
Copy Markdown
Member

@trygvrad thanks for the thorough discussions and for going through all the review rounds with us! This has been a lot of effort, but I'm convinced we're on a good path for consistently adding multivariate plotting so that it will be intuitive and easy to use within matplotlib. 🚀

@trygvrad

trygvrad commented Aug 2, 2026

Copy link
Copy Markdown
Contributor Author

Thank you @timhoffm.
The next step is that I rebase #31214 . I will probably get to it next weekend :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Development

Successfully merging this pull request may close these issues.

Imshow, pcolor and pcolormesh with Bivariate and Multivariate colormaps

5 participants