GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Sign in
  • ActionsCI/CD and automation
  • CodespacesCloud development environments
  • All Features
  • Documentation
  • Training
  • Certifications
  • Build with Copilot
  • Customize Copilot
  • Integrations
  • Trending repos
  • GitHub Sponsors
  • Developer articles
  • Enterprises

This view

  • Original page
  • New address

GeezerVison is showing github.com — Multivar colorbar by trygvrad · Pull Request #31214 · matplotlib/matplotlib

Conversation

@trygvrad

@trygvrad trygvrad commented Feb 27, 2026

Copy link
Copy Markdown
Contributor

This PR relates to Bivariate and Multivariate Colormapping, specifically #30527 Bivariate and multivariate colorbar.

This draft includes only BivarColorbar (not MultivarColorbar) but I want to make the PR now so that we can start discussing some of the questions.


import matplotlib.pyplot as plt
import numpy as np

fig, axes = plt.subplots(2, 2, figsize = (10, 6))
axes = axes.ravel()
locs = ['left', 'right', 'top', 'bottom']
im = np.random.random((2, 3, 4))
for i, loc in enumerate(locs):
    cim = axes[i].imshow(im, cmap='BiOrangeBlue')
    fig.bivar_colorbar(cim, location=loc, use_gridspec=False, fraction=0.3)
image

Note that the first axis is the y-axis and the second is the x-axis when the bivariate colormap is shown, following the implementation in #28454

@story645, @timhoffm, @ksunden
The main questions I have now are:

  1. I am thinking this should have a unique call signature, i.e. fig.bivar_colorbar(mappable) rather than branching inside fig.colorbar()
  2. The name bivar_colorbar is slightly awkward because it is not a bar, but more like a stamp. What do you think about this name?
  3. The Colorbar class has functions for customizing the ticks. To me the logic here is that a 1D colorbar does not have an x and y axis in the traditional sense, but a single axis, so it makes sense to have another layer of abstraction. BivarColorbar on the other hand has an x-axis and y-axis in the normal sense, and in light of this, how do we want users to customize the ticks?
    a. Not have functions on BivarColorbar for customizing ticks, and instead expect users to use BivarColorbar.ax.set_xticks etc.
    b. Make it possible to call the ax functions via the BivarColorbar, i.e. BivarColorbar.set_xticks
    c. Make functions that reflect the fact that BivarColorbar has a first and second axis matching the axes in the input to the mappable, i.e BivarColorbar.set_ticks(axis_0, axis_1)

I would really appreciate some feedback on 3. before I start making tests and typing :)

The placement of the bivariate colorbar uses nearly identical code to the normal colorbar. I assume this is what we want. [we will need more complicated placement for MultivarColorbar]

If you want to look at the code, I advise you to look at the commit 3cf2df9, rather than the PR, as this is built on top of #30597 – Multivar imshow.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
@github-actions github-actions Bot added topic: pyplot API topic: color/color & colormaps topic: images topic: collections and mappables topic: figures and subfigures topic: canvas and figure manager Documentation: API files in lib/ and doc/api labels Feb 27, 2026
@trygvrad trygvrad moved this to In Progress in Bivariate and Multivariate Colormapping Feb 27, 2026
@trygvrad trygvrad added this to Bivariate and Multivariate Colormapping Feb 27, 2026
@jklymak

jklymak commented Feb 28, 2026

Copy link
Copy Markdown
Member

I think this should take an ax and a cax argument, just like normal colorbar. For layout we are going to want the option for the colorbar to steal space from a number of axes, not just the axes that holds the mappable (though it's probably fine to default to that)

This should also play well with constrained layout. Tight layout won't work with colorbars so that is not a concern. If you need help with that, please ping. However conceptually it should be the same as a normal colorbar, it'll just possibly be much larger.

story645 and trygvrad reacted with thumbs up emoji
All reactions
  • 👍 2 reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@jklymak jklymak added the topic: geometry manager LayoutEngine, Constrained layout, Tight layout label Feb 28, 2026
@story645

story645 commented Mar 1, 2026 •
edited
Loading

Uh oh!

There was an error while loading. Please reload this page.

Copy link
Copy Markdown
Member
  1. Yes, agree on unique signature
  2. Especially since this is gonna be colorsquare/colorcircle/colorpane -> Esri just calls them legends far as I can tell (also this R library) but I think that'd maybe create too much collision here.

3. We might also/instead want the legend placement, especially for the geographic usecase. Which yes third parties can implement/might make more sense for cartopy to do.

trygvrad reacted with thumbs up emoji
All reactions
  • 👍 1 reaction

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@timhoffm

timhoffm commented Mar 2, 2026 •
edited
Loading

Uh oh!

There was an error while loading. Please reload this page.

Copy link
Copy Markdown
Member
  1. Yes

  2. bivar_colorbar seems ok. While it's a bit of a bulky "bar", I think the verbal connection to the 1D colorbar is still very valuable. All other names I can think of (color_square, color_patch, color_legend) all feel more off that stretching the meaning of "bar".

  3. (c) is out. AFAIK we don't have other cases where we lump dimensions together in such a way. It also makes it hard to set one axis only (would require some (None, yticks) acrobatics).
    (a) is appealing, because the API we'd have to define is really minimal. On the downside, it feels a bit too low-level to expose the full axes (yes Colorbar gives you access as well, but that's rather for advanced use cases). OTOH people know how to configure Axes, so much knowledge can ve transferred.
    (b) is appealing in that it has a strong analogy to Colorbar, but I'm afraid, we're putting in a lot of boilerplate for little gain.
    A fourth option (d) would be to expose properties colorbar.xaxis, colorbar.yaxis, which is still a quite minimal API, but gives access to all the axis configuration options, e.g. colorbar.xaxis.set_ticks(). The downside is that the Axis-level functions are less known than their high-level wrappers.

    Overall, I'm inclined to go with (d) - it's only a little more overhead than (a) but still quite minimal and feels cleaner. We could later still add some (b) functions if we see the need.

trygvrad reacted with thumbs up emoji
All reactions
  • 👍 1 reaction

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@story645

story645 commented Mar 2, 2026 •
edited
Loading

Uh oh!

There was an error while loading. Please reload this page.

Copy link
Copy Markdown
Member

Also apparently I didn't read 3 correctly 🤦‍♀️ but I agree w/ @timhoffm's proposal d)

trygvrad reacted with thumbs up emoji
All reactions
  • 👍 1 reaction

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@github-actions github-actions Bot added the status: needs rebase label Mar 2, 2026
@trygvrad
trygvrad force-pushed the multivar_colorbar branch from 3cf2df9 to 701c4b2 Compare March 4, 2026 22:22
@github-actions github-actions Bot removed the topic: geometry manager LayoutEngine, Constrained layout, Tight layout label Mar 4, 2026
@trygvrad

trygvrad commented Mar 5, 2026

Copy link
Copy Markdown
Contributor Author

This is progressing well, thank you for all the feedback so far.

In the draft, I am using

        self._image = self.ax.imshow(
            self.colorizer.cmap.lut,
            origin='lower',
            extent=(0, 1, 0, 1),
            transform=self.ax.transAxes,
            interpolation='nearest',
            alpha=self.alpha,
        )

to display the bivariate colorbar.

The transform=self.ax.transAxes is important because it allows us to use a different axis scale (i.e. log) without changing the image.
However, it has the side-effect that if one changes the limits on the axis, the axis changes while the norm stays the same.

This can for example be illustrated by:

norm = mpl.colors.MultiNorm(['log', 'log'])
ca = mpl.colorizer.Colorizer('BiOrangeBlue', norm)
fig, ax = plt.subplots()
cbar = fig.bivar_colorbar(ca, cax = ax, ticklocations=['left', 'bottom'])

norm.norms[1].vmin = 0.1
norm.norms[1].vmax = 10
cbar.xaxis.set_ticks([1, 10, 100])
print(ca.norm.vmin, ca.norm.vmax, cbar.ax.get_xlim() ) 
# (None, 0.1), (None, 10.0), (0.1, 100.0)

Where the xlim is changed but vmax on the norm is not.

My thinking is that any action taken by the user which would change the limits on the axis by operations on the axis, and not by setting the vmin or vmax, should raise an error of the form:

    raise ValueError("Changing the limits of this axis must be done "
            "using the norm attached to the bivariate colorbar.")

I can do this by connecting to "xlim_changed" and "ylim_changed".

Does this seem like a reasonable solution to you?


When ax.xaxis.set_ticks([1, 10, 100]) is called, it does not emit "xlim_changed". To me this seems like a bug. Should I make an issue?

story645 reacted with thumbs up emoji
All reactions
  • 👍 1 reaction

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@trygvrad trygvrad mentioned this pull request Mar 6, 2026
[Bug]: Changing limits by setting ticks does not emit "x/ylim_changed" #31247
Closed
@trygvrad
trygvrad force-pushed the multivar_colorbar branch from 701c4b2 to f19b42c Compare March 6, 2026 22:00
@jklymak

jklymak commented Mar 6, 2026

Copy link
Copy Markdown
Member

Wouldn't it make sense to use pcolormesh instead of image so users can change the scale if they want to?

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@trygvrad

trygvrad commented Mar 7, 2026

Copy link
Copy Markdown
Contributor Author

Wouldn't it make sense to use pcolormesh instead of image so users can change the scale if they want to?

Thank you for the feedback @jklymak, but I think we can work with imshow in this case.

The user should be able to change the scale on the norm, which will change the axis, but the bivariate colormap should render the same.

Consider these two figures:
image
image

Where the only change is that a LogNorm is used to when visualizing the GDP per capita in the lower plot.
This changes the way the data is mapped to color [the figure on the left], as well as the scale on the x-axis in the figure to the right, but the rendering of the colormap [in figure space] stays the same.

I do not think there is a need to set a scale different from that of the norm.

(additionally: I would like to avoid pcolormesh if possible, because if saved as an SVG, each tile in the mesh gets stored as a polygon, which makes it almost impossible to edit the svg later. I personally find the pipeline matplotlib→inkscape to be extremely powerful, and would like that to be possible here.)

The plots are based on https://trygvrad.github.io//mpl_docs/Bivariate%20colormaps.html

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@jklymak

jklymak commented Mar 7, 2026 •
edited
Loading

Uh oh!

There was an error while loading. Please reload this page.

Copy link
Copy Markdown
Member

People do ask to set the scale differently from the norm and they will be able to, but will get incorrect scaling of the colors. Pcolormesh can be rasterized, so you can avoid the polygon problem in svg/pdf. Note normal colorbar uses pcolormesh.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@trygvrad

trygvrad commented Mar 7, 2026

Copy link
Copy Markdown
Contributor Author

@jklymak thank you for the specification, I will see if I can make pcolormesh behave :)

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@trygvrad
trygvrad force-pushed the multivar_colorbar branch from f19b42c to 22e122d Compare March 7, 2026 22:29
@trygvrad

trygvrad commented Mar 12, 2026

Copy link
Copy Markdown
Contributor Author

@jklymak
I was able to implement pcolormesh in a suitable way, and I agree that this is a much better solution.

I am looking at what we need for layouts for multivar colorbar.
Fundamentally, I think we want something like this:

import matplotlib.pyplot as plt
import numpy as np

im_A = np.arange(200)[np.newaxis, :]*np.ones((200, 200))
im_B = np.arange(200)[:, np.newaxis]*np.ones((200, 200))
im_C = 0.9*im_A + 0.9*im_B

im_A = np.sin(im_A**0.5)**2
im_B = np.sin(im_B**0.5)**2
im_C = np.sin(im_C**0.5)**2

fig, axes = plt.subplots(2, 2, figsize = (10, 6))
axes = axes.ravel()
locs = ['left', 'right', 'top', 'bottom']

im = (im_A, im_B, im_C)
for i, loc in enumerate(locs):
    cim = axes[i].imshow(im, cmap='3VarAddA')
    cb = fig.multivar_colorbar(cim, location=loc, use_gridspec=False, fraction=0.3)
image

Where fig.multivar_colorbar returns a new class MultivarColorbar which is iterable, and the user can get [or iterate through] constituent Colorbar objects. All callbacks between the Artist [and Colorizer] and the colorbars is handled through the MultivarColorbar object.

In terms of layout, I want the user to have the option to organize the colorbars in a different way:

    cb = fig.multivar_colorbar(..., n_major=2)
image

or

    cb = fig.multivar_colorbar(..., n_major=1)
image

For the prototype I am basing this on bbox.splitx() and bbox.splity() [gridspec=False].

In the existing code there is make_axes and make_axes_gridspec. With this PR I think we will now need 6 versions:

make_axes
make_bivar_axes
make_multivar_axes
make_axes_gridspec
make_bivar_axes_gridspec
make_multivar_axes_gridspec [not yet implemented]

Because there is a lot of overlap, I have created a number of private helper functions, which (hopefully) avoids duplicate code.

@jklymak does this make sense to you in terms of the layout?
I would be happy to for any suggestions you have that can make this more elegant/suitable :)
My apologies that this PR is slightly difficult to read as it build upon #30597 rather than main.

I have not yet started looking at make_multivar_axes_gridspec. I suspect we can do something smart with the gridspec here. This is very much not my area of expertise, so some support here would be appreciated, but I think it would be best if we make sure we align on what is the intended behaviour first :)

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@jklymak

jklymak commented Mar 12, 2026

Copy link
Copy Markdown
Member

I'll need to look at what you are proposing. Ideally I think your colorbars would be encapsulated inside something constrained layout thinks is a "colorbar" that is should put into the colorbar slot in the layout manager. After that the "colorbar" can look as you like.

If you want the colorbars to be placed inside of the allotted area by the layout manager I think that will be more difficult

Overall I would not suggest making a gridspec version of the colorbars. I suppose those allow a non optimal but still legible layout.

I can try and look closer this weekend. If you had some example scripts for me to look at that'd make things easier. Apologies if they are already in this PR

trygvrad reacted with thumbs up emoji
All reactions
  • 👍 1 reaction

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@jklymak

jklymak commented Mar 12, 2026

Copy link
Copy Markdown
Member

bivar_colorbar seems to work fine with constrained layout.

mulitvar_colorbar doesn't work with

    return do_constrained_layout(fig, w_pad=w_pad, h_pad=h_pad,
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/jklymak/matplotlib/lib/matplotlib/_constrained_layout.py", line 116, in do_constrained_layout
    make_layout_margins(layoutgrids, fig, renderer, h_pad=h_pad,
  File "/Users/jklymak/matplotlib/lib/matplotlib/_constrained_layout.py", line 402, in make_layout_margins
    pad = colorbar_get_pad(layoutgrids, cbax)
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/jklymak/matplotlib/lib/matplotlib/_constrained_layout.py", line 822, in colorbar_get_pad
    parents = cax._colorbar_info['parents']
              ^^^^^^^^^^^^^^^^^^
AttributeError: 'MultivarColorbar' object has no attribute '_colorbar_info'. Did you mean: '_set_colorbar_info'?

which looks fixable?

trygvrad reacted with thumbs up emoji
All reactions
  • 👍 1 reaction

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@story645

story645 commented Mar 12, 2026

Copy link
Copy Markdown
Member

This might be the silliest thing but can we maybe flip the names to colorbar_{bi, multi}? I think would be especially easier for auto complete.

trygvrad and timhoffm reacted with thumbs up emoji
All reactions
  • 👍 2 reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@trygvrad

trygvrad commented Mar 14, 2026 •
edited
Loading

Uh oh!

There was an error while loading. Please reload this page.

Copy link
Copy Markdown
Contributor Author

I'll need to look at what you are proposing.

:D

Ideally I think your colorbars would be encapsulated inside something constrained layout thinks is a "colorbar" that is should put into the colorbar slot in the layout manager. After that the "colorbar" can look as you like.

@jklymak This is a good idea, and I will try to make it happen, but I am not super familiar with this part of the code. What kind of object do you think that something should be, because I don't think it should be an axis in an of itself, but it should probably show up in Figure.get_children() [?]

    def get_children(self):
        """Get a list of artists contained in the figure."""
        return [self.patch,
                *self.artists,
                *self._localaxes,
                *self.lines,
                *self.patches,
                *self.texts,
                *self.images,
                *self.legends,
                *self.subfigs]

bivar_colorbar seems to work fine with constrained layout. mulitvar_colorbar doesn't work with

Thanks for checking this, I will fix this and add some tests that cover these cases.


This might be the silliest thing but can we maybe flip the names to colorbar_{bi, multi}? I think would be especially easier for auto complete.

@story645 I think there is significant merit to this argument :)

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@trygvrad

trygvrad commented Apr 25, 2026

Copy link
Copy Markdown
Contributor Author

@jklymak @timhoffm @story645 I think this PR is ready for review.
Keep in mind that it builds upon #30597 rather than main, but I have rebased these onto main a couple of days ago.

I will start making PRs for #30529 and #30528 . These should be easier as they don't involve new API

story645 reacted with thumbs up emoji
All reactions
  • 👍 1 reaction

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@trygvrad

trygvrad commented May 24, 2026

Copy link
Copy Markdown
Contributor Author

@jklymak @timhoffm @story645 is it possible you have time to look at this in the coming weeks?

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@github-actions github-actions Bot added the status: needs rebase label May 28, 2026
@trygvrad trygvrad mentioned this pull request Jun 7, 2026
Multivar imshow #30597
Merged
2 tasks done
@trygvrad
trygvrad force-pushed the multivar_colorbar branch from 449bd74 to 65db055 Compare August 9, 2026 12:36
@github-actions github-actions Bot added status: needs rebase and removed status: needs rebase topic: images topic: collections and mappables labels Aug 9, 2026
@trygvrad

trygvrad commented Aug 9, 2026

Copy link
Copy Markdown
Contributor Author

Now that #30597 is merged, I have rebased this onto main, so that it can be reviewed.

@jklymak do you have time to review this, both in terms of functionality and the code?
The new top-level functionality is fig.colorbar_multivar() and fig.colorbar_bivar(). Let me know if you want me to include examples (in the docs) in this PR.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@trygvrad

trygvrad commented Sep 17, 2026

Copy link
Copy Markdown
Contributor Author

I hope its ok if I ask you to take a look at this @rcomer , I was told by @story645 that you have done some PRs that interact with the layout engine.

This PR is the last code-heavy PR required to get multivariate and bivariate color mapping to work, a project that has been ongoing since 2024 Bivariate and Multivariate Colormapping. We are on the last mile, but I need someone to review this PR, preferably someone with experience with the layout engine.

In short, this PR allows you to make the 2d colorbar for the following plot, using the new Figure.colorbar_bivar(mappable) member function on the Figure class.
image
And similar for multivariate colorbars using Figure.colorbar_multivar(mappable).

If you would be willing to review this PR that would be very much appreciated 🥇

The first question is:

  • Does this PR use the layout engine in a sensible way?

Other than that I think it is mostly a question of if there are any more things/edge cases we should test.

Let me know if you have any questions :)

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@rcomer

rcomer commented Sep 18, 2026

Copy link
Copy Markdown
Member

Hi @trygvrad, I'm sorry but I do not have the capacity to be able to take this on, at least not for the next couple of months.

story645 and trygvrad reacted with thumbs up emoji
All reactions
  • 👍 2 reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@trygvrad

trygvrad commented Sep 24, 2026

Copy link
Copy Markdown
Contributor Author

Thank you for the quick reply @rcomer

@story645 I wanted to go through this in the meeting today, but I don't have the mental capacity to attend today. Could you put it on the agenda for next week?
I will prepare a summary of the PR for next week so that we can more easily go through it.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@story645

story645 commented Sep 24, 2026

Copy link
Copy Markdown
Member

No problem, will do!

trygvrad reacted with heart emoji
All reactions
  • ❤️ 1 reaction

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

trygvrad added 4 commits September 30, 2026 20:45
@trygvrad
Initial bivar_colorbar
cce7d7a
@trygvrad
initial multivar colorbar
c566257
@trygvrad
working on constrained_layout
d868a0e
@trygvrad
tests for colorbar_multivar
69b7d87
multivar_colorbar: updates after rebase

multivar_colorbar: fix for docs
@trygvrad
trygvrad force-pushed the multivar_colorbar branch from 2e57ffa to a78ac46 Compare September 30, 2026 20:49
@trygvrad
minor bugfixes
c353303
@trygvrad
trygvrad force-pushed the multivar_colorbar branch from a78ac46 to c353303 Compare October 1, 2026 14:26
@trygvrad

trygvrad commented Oct 1, 2026

Copy link
Copy Markdown
Contributor Author

This is a summary of what this PR does, so that it is easier to review:

New top level functionality

  • Two new figure methods, fig.colorbar_bivar() and fig.colorbar_multivar().
  • fig.colorbar_bivar()draws a 2D colormap as a square "stamp" next to the axes, not as a strip. It follows the convention from PR MultivarColormap and BivarColormap #28454: the first data axis is shown on the y-axis and the second on the x-axis.
  • fig.colorbar_multivar() draws a separate colorbar for each variable in the multivariate case. You can arrange these in rows and colums by specifying the number of colorbars along the long axis.

New classes

  • BivarColorbar – works mostly same as Colorbar only with two axes instead of one.
    Layout engine reserves space in a manner similar to colorbar
    The scale of the axes follow from the norm, same as with colorbar
    Ticklocations based on placement
    Interactive pan and zoom edit both norms directly _get_view, _set_view, _set_view_from_bbox, drag_pan
    Layout engine resizes axes when figure is resized
  • MultivarColorbar(Sequence)
    A Sequence of Colorbar objects.
    Is not associated with a single Axes, but each constituent Colorbar will have its own axes
    Individual Colorbar accessed by indexing, i.e. fig.colorbar_multivar()[2].minorticks_off()
    Creates new axes if not provided using the caxes keyword: fig.colorbar_multivar(mappable) will create new axes for the colorbars, while fig.colorbar_multivar(mappable, caxes=axes) will place the colorbars in the axes provided.
    When creating new axes, the grid layout can be customized (rows/columns, spacing) but finer control is offered by passing a list of Axes.

Axes creation helpers and organization

  • In colorbar.py, make_axes and make_axes_gridspec are split into shared pieces
  • When dynamically resizing a plot the axes with the mappable need to keep track of what colorbars it is associated with. This is handled by the _colorbars property on the axes with the mappable. This contains objects that have a _colorbar_info dict. This dict now has a "type" key, and the behaviour branches on the type ('Colorbar', 'BivarColorbar' or 'MultivarColorbar'). For Colorbar and BivarColorbar the _colorbar_info exists on the associated axes, and contains the parameters required for positioning and scaling (aspect etc.). MultivarColorbar is not associated with a single axes so the _colorbar_info instead exists on the MultivarColorbar object itself, but performs the same role in terms of the layout engine.
  • For a multivariate colorbar, they use the container's tight bbox and its original position, then give each colorbar axes its own sub-bbox from _get_tight_packing_inside.

Minimal example

import matplotlib.pyplot as plt
import numpy as np

im_A = np.arange(12).reshape((3, 4)) % 4
im_B = np.arange(12).reshape((3, 4)) % 3
im_C = np.arange(12).reshape((3, 4)) % 5
fig, axes = plt.subplots(1, 2, figsize=(12, 4))
# -------  colorbar_multivar  ------------
mim = axes[0].imshow((im_A, im_B, im_C), cmap='3VarAddA')
cbs = fig.colorbar_multivar(mim)
cbs[0].set_label('A')
mim.norm.vmin = (-1, -1, -1)
# -------  colorbar_bivar  ------------
mim = axes[1].imshow((im_A, im_B), cmap='BiOrangeBlue')
cbs = fig.colorbar_bivar(mim)
cbs.set_ylabel('A')
mim.norm.vmin = (-1, -1)
image

Positioning example colorbar_multivar()

im_A = np.arange(200)[np.newaxis, :]*np.ones((200, 200))
im_B = np.arange(200)[:, np.newaxis]*np.ones((200, 200))
im_C = 0.9*im_A + 0.9*im_B

im_A = np.sin(im_A**0.5)**2
im_B = np.sin(im_B**0.5)**2
im_C = np.sin(im_C**0.5)**2

fig, axes = plt.subplots(2, 2, figsize = (10, 6), dpi = 100, layout="constrained")
axes = axes.ravel()
locs = ['right', 'left',  'top', 'bottom']

im = (im_A, im_B, im_C)
for i, loc in enumerate(locs):
    cim = axes[i].pcolormesh(im, cmap='3VarAddA', vmax=(1, 1, 1))
    cb = fig.colorbar_multivar(cim, location=loc, n_major=2)
    cb[0].set_label('A')
    cb[1].set_label('B')
    cb[2].set_label('C')

fig.show()
image

Positioning example colorbar_bivar()

im_A = np.arange(200)[np.newaxis, :]*np.ones((200, 200))
im_B = np.arange(200)[:, np.newaxis]*np.ones((200, 200))

im_A = np.sin(im_A**0.5)**2
im_B = np.sin(im_B**0.5)**2

fig, axes = plt.subplots(2, 2, figsize = (10, 6), dpi = 100, layout="constrained")
axes = axes.ravel()
locs = ['right', 'left',  'top', 'bottom']

im = (im_A, im_B)
for i, loc in enumerate(locs):
    cim = axes[i].pcolormesh(im, cmap='BiOrangeBlue', vmax=(1, 1))
    cb = fig.colorbar_bivar(cim, location=loc, fraction = 0.3)
    cb.set_ylabel('A')
    cb.set_xlabel('B')

fig.show()
image

The last two examples use layout="constrained", while the first example does not.
Using layout="constrained" makes it easier to prevent labels from overlapping figures, etc, as the figure becomes crowded.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

timhoffm
timhoffm reviewed Oct 1, 2026
View reviewed changes

@timhoffm timhoffm left a comment

Copy link
Copy Markdown
Member

There was a problem hiding this comment.

Choose a reason for hiding this comment

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

I'll do incremental reviews, because it's too large to do it in one go.

@trygvrad You may wait for the full review or already react to the comments. If you want to add changes, for now please only add new commits (no rebase or squash) so that the state of today can still be created by selecting the commits up to today. Otherwise it's hard for me to track what I have reviewed and which changes are implemented since then.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

trygvrad reacted with thumbs up emoji
All reactions
  • 👍 1 reaction
Comment thread lib/matplotlib/colorbar.py
def __init__(
self, ax, mappable,
*,
alpha=None,

@timhoffm timhoffm Oct 1, 2026

Copy link
Copy Markdown
Member

There was a problem hiding this comment.

Choose a reason for hiding this comment

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

I know we have this in Colorbar. But do you understand what this is used for? It seems like a semi-transparent colorbar is quite unhelpful because it distorts the colors of the colormap. If we don't see a use case, I'd rather not include this to keep things simple.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions

@trygvrad trygvrad Nov 5, 2026

Copy link
Copy Markdown
Contributor Author

There was a problem hiding this comment.

Choose a reason for hiding this comment

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

That's a good point, I agree 👍

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
Comment thread lib/matplotlib/colorbar.py
Comment on lines +1402 to +1403
Unlike `Colorbar`, `BivarColorbar` does not support
customizing the ticks, and ticks must be customized on the axes instead.

@timhoffm timhoffm Oct 1, 2026

Copy link
Copy Markdown
Member

There was a problem hiding this comment.

Choose a reason for hiding this comment

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

A bit more specific:

BivarColorbar uses a regular Axes as underlying Artist.
Unlike Colorbar, BivarColorbar does not support
customizing the ticks via a high-level API. Instead, customize ticks
directly on the underlying Axes BivarColorbar.ax.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

trygvrad reacted with thumbs up emoji
All reactions
  • 👍 1 reaction
Comment thread lib/matplotlib/colorbar.py
Comment on lines +1442 to +1449
self.ax = ax
self.location = location
if isinstance(mappable, mpl.colorizer.Colorizer):
mappable = mcolorizer.ColorizingArtist(mappable)

self.mappable = mappable
self.aspect = aspect
self.colorizer = mappable.colorizer

@timhoffm timhoffm Oct 1, 2026

Copy link
Copy Markdown
Member

There was a problem hiding this comment.

Choose a reason for hiding this comment

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

Should they all public? Which of these are safe to modify?

If no read access is needed, we could make them private. If reading is needed but writing should not be allowed, we should make the variable private and expose it though a public property.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions

@trygvrad trygvrad Nov 5, 2026 •
edited
Loading

Uh oh!

There was an error while loading. Please reload this page.

Copy link
Copy Markdown
Contributor Author

There was a problem hiding this comment.

Choose a reason for hiding this comment

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

This is a good point, I think we can make them private for now, until we find a use case to make them public

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
Comment thread lib/matplotlib/colorbar.py
ticklocations : tuple describing the ticklocation of the y and x axis
The first element must be {'auto', 'left', 'right'}
The second element must be {'auto', 'top', 'bottom''}.
If 'auto', the ticklocations are determined by the *location*.

@timhoffm timhoffm Oct 1, 2026

Copy link
Copy Markdown
Member

There was a problem hiding this comment.

Choose a reason for hiding this comment

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

Minor, but reads more specific without having to explain details 😄

Suggested change
If 'auto', the ticklocations are determined by the *location*.
If 'auto', a reasonable default is chosen based on *location*.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

trygvrad reacted with thumbs up emoji
All reactions
  • 👍 1 reaction
Comment thread lib/matplotlib/colorbar.py
Comment on lines +1463 to +1467
location_ticklocs = _get_bivar_ticklocations_from_location(location)
for i in range(2):
if ticklocations[i] == 'auto':
ticklocations[i] = location_ticklocs[i]
self.ticklocations = ticklocations

@timhoffm timhoffm Oct 1, 2026

Copy link
Copy Markdown
Member

There was a problem hiding this comment.

Choose a reason for hiding this comment

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

I would give the helper function a little more responsibility, so that we can here write:

self.ticklocations = _resolve_bivar_ticklocations(ticklocations, location)

read: the self.ticklocations are determined by resolving the parameters ticklocations and location.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

trygvrad reacted with thumbs up emoji
All reactions
  • 👍 1 reaction
Comment thread lib/matplotlib/colorbar.py Outdated
Comment on lines +1586 to +1614
"""
def _y_limits_changed(self, ax=None):
if not self._allow_change_limits:
ylim = self.ax.get_ylim()
if self.colorizer.norm.norms[0].vmin != ylim[0]:
self.colorizer.norm.norms[0].vmin = ylim[0]
if self.colorizer.norm.norms[0].vmax != ylim[1]:
self.colorizer.norm.norms[0].vmax = ylim[1]

def _x_limits_changed(self, ax=None):
if not self._allow_change_limits:
xlim = self.ax.get_xlim()
if self.colorizer.norm.norms[1].vmin != xlim[0]:
self.colorizer.norm.norms[1].vmin = xlim[0]
if self.colorizer.norm.norms[1].vmax != xlim[1]:
self.colorizer.norm.norms[1].vmax = xlim[1]
"""
"""
def _update_limits(self, ax=None):
if not self._allow_change_limits:
ylim = self.ax.get_ylim()
xlim = self.ax.get_xlim()
if (self.colorizer.norm.norms[0].vmin != ylim[0] or
self.colorizer.norm.norms[0].vmax != ylim[1] or
self.colorizer.norm.norms[1].vmin != xlim[0] or
self.colorizer.norm.norms[1].vmax != xlim[1]):
raise ValueError("Changing the limits of this axis must be done "
"using the norm attached to the bivariate colorbar.")
"""

@timhoffm timhoffm Oct 1, 2026

Copy link
Copy Markdown
Member

There was a problem hiding this comment.

Choose a reason for hiding this comment

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

Comment our by string quoting ?!?

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions

@trygvrad trygvrad Nov 5, 2026

Copy link
Copy Markdown
Contributor Author

There was a problem hiding this comment.

Choose a reason for hiding this comment

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

This is somewhat embarrassing 😅

This should be outdated, are you reviewing the commits, or the PR as a whole. I think this needs to be squashed. Let me know if you want me to squash it now.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
Comment thread lib/matplotlib/colorbar.py Outdated

def update_normals(self, mappable=None):
self.set_alpha(self.mappable.get_alpha())
#self._draw_all()

@timhoffm timhoffm Oct 1, 2026

Copy link
Copy Markdown
Member

There was a problem hiding this comment.

Choose a reason for hiding this comment

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

Please check commented out code. Either uncomment or delete.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

trygvrad reacted with thumbs up emoji
All reactions
  • 👍 1 reaction
Comment thread lib/matplotlib/colorbar.py Outdated
Comment on lines +1646 to +1647
# self.ax.callbacks.disconnect(self._colorbar_xax_cid)
# self.ax.callbacks.disconnect(self._colorbar_yax_cid)

@timhoffm timhoffm Oct 1, 2026

Copy link
Copy Markdown
Member

There was a problem hiding this comment.

Choose a reason for hiding this comment

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

Please check commented out code. Either uncomment or delete.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

trygvrad reacted with thumbs up emoji
All reactions
  • 👍 1 reaction
Comment thread lib/matplotlib/colorbar.py
self._interactive_funcs = ["_get_view", "_set_view",
"_set_view_from_bbox", "drag_pan"]
for x in self._interactive_funcs:
setattr(self.ax, x, getattr(self, x))

@timhoffm timhoffm Oct 1, 2026

Copy link
Copy Markdown
Member

There was a problem hiding this comment.

Choose a reason for hiding this comment

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

Oh boy, we are picking up all the design sins of Colorbar :). Let's at least make this a concept and define

def _patch_ax_methods(self):
    """Override Axes for colorbar-specific behavior."""
    self.ax.cla = self._cbar_cla
    # interactive methods
    self.ax._get_view = self._get_view
    self.ax._set_view = self._set_view
    self.ax._set_view_from_bbox = self._set_view_from_bbox
    self.ax.drag_pan = self._drag_pan
    
def _unpatch_ax_methods(self):
    """Undo override Axes for colorbar-specific behavior."""
    # Note: del works because we added these methods as
    # instance attributes, hiding the class-level methods.
    # Deleting the attributes uncovers the methods.
    del self.ax.cla
    # interactive methods
    del self.ax._get_view
    del self.ax._set_view
    del self.ax._set_view_from_bbox
    del self.ax.drag_pan

That's far more readable. Note also that we are now able to keep self._drag_pan private even though we must override a public method ax.drag_pan.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

trygvrad reacted with thumbs up emoji
All reactions
  • 👍 1 reaction
Comment thread lib/matplotlib/colorbar.py
@aspect.setter
def aspect(self, aspect):
aspect = float(aspect)
self._aspect = aspect

@timhoffm timhoffm Oct 1, 2026

Copy link
Copy Markdown
Member

There was a problem hiding this comment.

Choose a reason for hiding this comment

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

Do we need to store this, or could we reconstruct it from ax.get_box_aspect() when needed?

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions

@trygvrad trygvrad Nov 5, 2026

Copy link
Copy Markdown
Contributor Author

There was a problem hiding this comment.

Choose a reason for hiding this comment

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

We can remove this, I did some interactive testing with an assertion that these are equal, and it seems to work in all cases, and no tests fail.

Good catch!

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
@trygvrad

trygvrad commented Nov 6, 2026 •
edited
Loading

Uh oh!

There was an error while loading. Please reload this page.

Copy link
Copy Markdown
Contributor Author

I'll do incremental reviews, because it's too large to do it in one go.

@timhoffm Do you want to look more at this, or should I make a commit with fixes?

I'll try to show up for next weeks meeting

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@timhoffm

timhoffm commented Nov 10, 2026

Copy link
Copy Markdown
Member

@trygvrad Sorry, this is really hard to review. There's too much going on, and it's very hard to keep an overview. I tried to narrow the scope by looking at individual commits, but that doesn't work either because the commits revise each other.

Would it be possible to split this into multiple subsequent PRs? E.g.

  • Optional preparatory refactoring (if so just argue why we want the change)
  • Bivar colorbar
  • Multivar colorbar
  • Optional fix constrained_layout

Or whatever helps to make smaller chunks. AI should be able to sort this if you give reasonalbe direction.

If that sounds like a reasonable approach, please apply my comments so far, and then let's split the result.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@jklymak

jklymak commented Nov 10, 2026

Copy link
Copy Markdown
Member

I also question if it is necessary to touch colorbar.py or if this could be independent. I appreciate there is some wrangling in constrained_layout, but that seems minor. If it were independent, then it would make the review burden substantially less since we would be sure you aren't poking into the delicate dance that is normal matplotlib colorbars.

For what it is worth, I think your three colorbars should just be three colorbars, perhaps wrapped in a helper.

The bi-variate colorbar should just be an axes, and probably should just be treated like a normal axes rather than a colorbar. It really takes up too much room to be a bare decoration.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@timhoffm

timhoffm commented Nov 10, 2026

Copy link
Copy Markdown
Member

For what it is worth, I think your three colorbars should just be three colorbars, perhaps wrapped in a helper.

The bi-variate colorbar should just be an axes, and probably should just be treated like a normal axes rather than a colorbar. It really takes up too much room to be a bare decoration.

Not sure. Do we have reasonable capability to layout multiple colorbars next to an Axes?
For bi-variate: Positioning is complex. It seems like it’s often used for geographical plots and thee as an inset. Aa a baseline, I’d still say that treating as a decoration is reasonable. Bivar colormaps should typically be square and smaller than the data Axes. That’s not a good fit for a regular Axes layout.

jklymak reacted with thumbs up emoji
All reactions
  • 👍 1 reaction

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@jklymak

jklymak commented Nov 11, 2026

Copy link
Copy Markdown
Member

I don't think we have a turnkey way to make three colorbars and lay them out, so a helper is great.

For bivariate, a colorbar axes does three things.

  1. makes a colormap graphic to fill the axes
  2. labels just one axis, and removes ticks and labels from the minor axis
  3. Participates in layout in (probably too many) various ways.
    a. Steals space via gridspec nesting
    b. steals space and is just placed beside the axes using positioning
    i. is a fixed layout
    ii. is moved around by constrained layout.

1 and 2 seems like they are easily separated. Without looking too hard I think this PR does that, or if it doesn't should try and do so as much as possible.

3 is obviously a bit convoluted, and maybe requires the interaction between the new and traditional colorbars.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@trygvrad

trygvrad commented Nov 12, 2026

Copy link
Copy Markdown
Contributor Author

Thank you for the feedback @timhoffm @jklymak :)

Or whatever helps to make smaller chunks. AI should be able to sort this if you give reasonalbe direction.

I will try to do this, asking AI is probably a good idea.

I also question if it is necessary to touch colorbar.py

Might as well also see if I can make some new files instead of touching colorbar.py, that might make it easier to review as well.

story645 and timhoffm reacted with thumbs up emoji
All reactions
  • 👍 2 reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

This branch has not been deployed

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

Reviewers

@timhoffm timhoffm timhoffm left review comments

At least 1 approving review is required to merge this pull request.

Uh oh!

There was an error while loading. Please reload this page.

Assignees

No one assigned

Labels

Documentation: API files in lib/ and doc/api topic: canvas and figure manager topic: color/color & colormaps topic: figures and subfigures topic: geometry manager LayoutEngine, Constrained layout, Tight layout topic: mplot3d topic: pyplot API

Projects

Bivariate and Multivariate Colormapping
Status: In Progress

Milestone

No milestone

Development

Successfully merging this pull request may close these issues.

Uh oh!

There was an error while loading. Please reload this page.

5 participants

@trygvrad @jklymak @story645 @timhoffm @rcomer
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
You are visitor 01402520 — GeezerVison does not store the pages you look up.
Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.