X Tutup
Skip to content

Multivar imshow#30597

Open
trygvrad wants to merge 9 commits intomatplotlib:mainfrom
trygvrad:multivar_imshow
Open

Multivar imshow#30597
trygvrad wants to merge 9 commits intomatplotlib:mainfrom
trygvrad:multivar_imshow

Conversation

@trygvrad
Copy link
Contributor

@trygvrad trygvrad commented Sep 24, 2025

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
Copy link
Contributor Author

trygvrad commented Nov 2, 2025

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

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
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
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
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
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
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?

Copy link
Member

@ksunden ksunden left a comment

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

@trygvrad
Copy link
Contributor Author

trygvrad commented Dec 1, 2025

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?

Copy link
Member

@story645 story645 left a comment

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.

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

@story645
Copy link
Member

story645 commented Jan 6, 2026

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

Yeah I think since this is new and provisional, it's okay to encourage folks to use something else. But I would document the something else "for multivariate data use x,y,z" instead of just saying it's available.

Also attn @timhoffm since this is an API question.

@timhoffm
Copy link
Member

timhoffm commented Jan 7, 2026

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

Yes. Would it help for typing to differentiate 1d and Nd norm types? This could maybe help with overloading, so that we can more systematically separate the cases? But I'm not too much of a typing expert here and also have no oversight where this is used; e.g. it doesn't help if you store an arbitrary norm in an attribute and later query its limits. Maybe this approach would need to be extended to Colorizer to take effect?? Not clear whether that's worth it.

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

Agree.

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

[...]

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?

Yes, let's keep the _ColorizingInterface more narrow for now. Multi-dim mapping can be accessed through the .colorizer attribute. This is a variant of "favor composition over inheritance" - We don't have to expose the full colorizing behavior with its added complixity on the artist, but can leave that to the associated Colorizer.

@ksunden ksunden dismissed their stale review January 15, 2026 20:07

Comments regarding typing have been addressed

@trygvrad trygvrad force-pushed the multivar_imshow branch 2 times, most recently from 6f2dc67 to ea43447 Compare February 12, 2026 22:23
@trygvrad
Copy link
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 :)

- (M, N): an image with scalar data. The values are mapped to
colors using normalization and a colormap. See parameters *norm*,
*cmap*, *vmin*, *vmax*.
- (v, M, N): if coupled with a cmap that supports v scalars
Copy link
Member

Choose a reason for hiding this comment

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

I probably should remember, but what is v the abbreviation for? - And since I don't: is ist important? It's just the number of components. Would K be a better name?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Colormaps have an attribute n_variates, so "v" stands for variates.
But let us change it to K, as I think the logical jump to justify v is too long.

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

@trygvrad
Copy link
Contributor Author

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

Copy link
Member

@timhoffm timhoffm left a comment

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.

Co-authored-by: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com>
@trygvrad
Copy link
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
Copy link
Member

story645 commented Mar 1, 2026

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
Copy link
Member

timhoffm commented Mar 1, 2026

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.

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

X Tutup