Skip to content

Multivar imshow#30597

Open
trygvrad wants to merge 10 commits into
matplotlib:mainfrom
trygvrad:multivar_imshow
Open

Multivar imshow#30597
trygvrad wants to merge 10 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 Outdated
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

Copy link
Copy Markdown
Contributor Author

@timhoffm @ksunden @story645
I think the remaining test failures are not related to the changes here, and that this PR is ready to be approved/merged.

The next step in Bivariate and Multivariate Colormapping (view) will be to add equivalents to fig.colorbar(), and I will start working on this, and make a draft PR such that we can start discussing API names, layout etc :)

Comment thread lib/matplotlib/axes/_axes.py Outdated
Comment thread lib/matplotlib/axes/_axes.py
Comment thread lib/matplotlib/tests/test_axes.py Outdated
fig, axes = plt.subplots(1, 6, figsize=(10, 2))

axes[0].imshow((x_0, x_1), cmap='BiPeak', interpolation='nearest')
axes[1].matshow((x_0, x_1), cmap='BiPeak')

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.

Do we want matshow to support multivar colors? Technically, that's possible, because it's a wrapper around imshow, but semantically it's currently defined as "Plot the values of a 2D matrix or array as color-coded image."

I haven't thought much about this, but it seems that the multivar extension is not helpful for matrix visualization. If that's the case, we should enforce in matshow that no multivar input is supported.

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.

My thinking was that matshow is not necessary to include in the first iteration. If we want to introduce it later we can make an issue. Not sure if it would be a "good first issue" or not, but it might yield the additional benefit of having someone else familiarize themselves with this side of the project.

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.

Agreed, a formal introduction to matshow needs additional consideration and should be investigated separately.

the question here is whether we implicitly allow multivariate mapping by doing nothing, or whether we explicitly check and prohibit multivariate mapping in matshow. I’m inclined towards the latter.

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.

matshow() starts with a line:

        Z = np.asanyarray(Z)

So since we can guarantee that the output is a np.array, we can easily check the size.
I added the following check.

        Z = np.asanyarray(Z)
        if Z.ndim != 2:
            if Z.ndim != 3 or Z.shape[2] not in (1, 3, 4):
                raise TypeError(f"Invalid shape {Z.shape} for image data")

(This allows data of shape (M, N, 1), as I found that that is currently allowed on main).

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.

but it seems that the multivar extension is not helpful for matrix visualization

Someone could be visualizing index variables in a matrix same as they're visualizing index variables in a choropleth. I question if it's a good idea, but I think the general philosophy of this library should be to allow anything that isn't explicitly wrong.

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

Copy link
Copy Markdown
Contributor Author

@timhoffm I pushed the requested changes, thank you for the feedback :D

@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.

Only two minor style issues.

This is fundamentally ready, but I think we cannot yet merge because we haven't branched 3.12 off yet (everything on main still targets 3.11).

This by itself should not go in without #30527 and potentially the other remaining topics in the project: https://github.com/orgs/matplotlib/projects/9/views/1.

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

Copy link
Copy Markdown
Contributor Author

This is fundamentally ready, but I think we cannot yet merge because we haven't branched 3.12 off yet (everything on main still targets 3.11).

:D

This by itself should not go in without #30527 and potentially the other remaining topics in the project: Bivariate and Multivariate Colormapping (view).

Agreed

@trygvrad trygvrad mentioned this pull request Feb 27, 2026
@trygvrad trygvrad moved this from In Progress to Ready to be merged in Bivariate and Multivariate Colormapping Feb 27, 2026
@story645

story645 commented Mar 1, 2026

Copy link
Copy Markdown
Member

This by itself should not go in without #30527 and potentially the other remaining topics in the project: Bivariate and Multivariate Colormapping (view).\n\nAgreed

Since there are a bunch of things that should go in together, do we want to experiment with a feature branch? I know we initially rejected the idea, but @QuLogic seems to be having some success with the font-overhaul branch.

@timhoffm

timhoffm commented Mar 1, 2026

Copy link
Copy Markdown
Member

I'm -0.2 on feature branches. The advantage is that you can commit parts and then base other work on this without affecting the main branch. But this is also the disadvantage: If the feature changes code in areas that is also touched by the main branch, merging gets a nightmare quickly. The text overhaul branch works well because is in a very particular part of the code (and generally every change we make in that area is likely to change text rendering so should be on that branch).

Multivar colormapping more broadly touches the code and is less suited for a feature branch. So far, we've been quite successful in incremental merges to main. It's just that we don't want to expose a multivar imshow as long as we don't have a suitable colorbar for that. And timing is accidentally so that the 3.11 release is between the readiness of the two parts.

@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

%(cmap_doc)s

%(multi_cmap_doc)s

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.

multi_cmap_doc should be rewritten from

Multivariate data is only accepted if a multivariate colormap (BivarColormap or MultivarColormap) is used.

to

Multivariate colormaps (BivarColormap or MultivarColormap) require multivariate data.

because we should phrase from the perspective of the documented parameter (here: cmap).


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

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?

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

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

Projects

Development

Successfully merging this pull request may close these issues.

Imshow, pcolor and pcolormesh with Bivariate and Multivariate colormaps

5 participants