Skip to content

Draft for multivariate and bivariate colormaps #26996

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 23 commits into
base: main
Choose a base branch
from

Conversation

trygvrad
Copy link
Contributor

@trygvrad trygvrad commented Oct 4, 2023

PR summary

This draft PR is a reply to #14168 Feature request: Bivariate colormapping

The context of this draft PR is the discussion thread for #14168 and the weekly meeting Sep 15th 2023.
It was suggested that a class VectorMappable should be a drop-in replacement for ScalarMappable.
And it would be of interest to see how VectorMappable and multivariate colormaps can be threaded through the existing api.
It was further suggested that the new functionality should be triggered by providing a multivariate or bivariate colormap.
This ensures that the new functionality is only triggered when the user shows intent.

This draft PR is intended to allow for further discussion of #14168 at the next few weekly meetings.

The following functionality is supported:

Multivariate colormaps:

import matplotlib.pyplot as plt
A = plt.imread('R.png')
B = plt.imread('G.png')
C = plt.imread('B.png')

fig, axes = plt.subplots(1, 2, figsize = (12,4))

pm = axes[0].pcolormesh((A,B,C), cmap = '3VarSubA', vmax = (0.4, 0.6, 0.5))
cb0, cb1, cb2 = fig.colorbars(pm, shape = (-1,2))
axes[0].set_title('Subtractive multivariate colormap')

pm = axes[1].pcolormesh((A,B,C), cmap = '3VarAddA', vmax = (0.4, 0.6, 0.5))
cb0, cb1, cb2 = fig.colorbars(pm, shape = (-1,2))
axes[1].set_title('Additive multivariate colormap')

multivariate colormaps
(data from https://arxiv.org/abs/1812.10366)

Bivariate colormaps:

im0, im1 = get_random_data()
fig, axes = plt.subplots(1, 2, figsize = (12,4))

pm = axes[0].pcolormesh((im0,im1), cmap = 'BiOrangeBlue', vmin = -1, vmax = 1)
cax = fig.colorbar_2D(pm, shape = (-1,2))
axes[0].set_title('Square 2d colormap')

pm = axes[1].pcolormesh((im0,im1), cmap = 'BiCone', vmin = -1, vmax = 1)
cax = fig.colorbar_2D(pm, shape = (-1,2))
axes[1].set_title('Circular 2d colormap')

multivariate colormaps

Minimum changes required

This implementation is designed to make minimal changes to the existing code.
I have tried to list the significant changes bellow:

cm.VectorMappable is a drop in replacement for cm.ScalarMappable.
VectorMappable uses delegation and contains one or more ScalarMappables (.scalars).

A new function axes._base.ensure_cmap(cmap) is added.
ensure_cmap(cmap) takes a colormap, string or None and returns a Colormap, MultivarColormap, or BivarColormap object.
None will always return the default (1D) colormap. i.e. to use bivariate or multivariate data, the user must show intent by actively choosing a suitable colormap.

A new function axes._base.ensure_multivariate_norm(n_variates, data, norm, vmin, vmax) is added.
When called, this ensures that data, norm, vmin, and vmax all have length n_variates. Single arguments are repeated if neccessary.

The new member varaible n_variates is accessible on all Colormap, BivarColormap, and MultivarColormap objects. It is 1, 2 or n, respectively.

Figure.pcolormesh() is adapted to support multivariate data by the following four changes:

  1. collections.Collection now inherits from cm.VectorMappable instead of cm.ScalarMappable
  2. cmap = ensure_cmap(cmap) is called immediately in Figure.pcolormesh(). This gives access to cmap.n_variates.
  3. If cmap.n_variates > 1, the function ensure_multivariate_norm() is called to ensure the correct length of the norm parameters
  4. axes._pcolorargs() takes an additional keyword argument (n_variates), so that if n_variates > 1, it can correctly extract the dimensions from the input data.

Implementation of multivariate and bivariate colormaps

Multivariate data (2-8 variates) is supported vi the class colors.MultivarColormap.
MultivarColormap is iterable, and iterating on MultivarColormap returns (1D) Colormap objects.
MultivarColormap.combination_mode is either 'Add' or 'Sub', and this determines if the colormaps are are combined by adding or subtracting the RGB values.
The file _cm_listed_multivar contains new 1D colormaps to bulid the multivariate colormaps.
The multivariate colormaps are contained in a cm.ColormapRegistry() accessible at mpl.multivar_colormaps

2D colormaps are supported via the class colors.BivarColormap.
There are two subclasses: SegmentedBivarColormap and BivarColormapFromImage
SegmentedBivarColormap repurposes _image.resample() to extrapolate a smaller image to a larger image. By using this we do not need to store the full (256,256,3) LUT in the source files, but can get away with (65,65,3) for complex colormaps or (9,9,3) for more simple colormaps with no significant loss in fidelity.
BivarColormap.shape is either 'square' or 'circle'. This changes how values outside the colormap are interpolated onto the colormap.
The bivariate colormaps are contained in a cm.ColormapRegistry() accessible at mpl.bivar_colormaps

Further work

There are a number of further topics that need to be worked on to make this more than a draft PR (as lited below).
However I think it would be conducive to first discuss the points above, and whether the design desicions made so far, are sensible.

  1. There is an absolutely minimal implementation of Figure.colorbars() and Figure.colorbar_2D().
    These are only for illustration purposes, and will need to be completely rewritten, and we need to think about return types.
  2. I have not tested multi/bivariate colormaps with different kinds of norms (log, etc.). I suspect it already works, but it will need to be supported in figure.colorbar_2D().
  3. Need to implement multivariate functionality for (all?, subset of?) other functions that take cmap [scatter, hexbin, imshow, pcolor, pcolorfast, specgram, contour, contourf, hist2d, matshow, others?].
  4. Ensure docstrings are suitable for the new classes and altered functions.
  5. I designed a number colormaps, but we should have a discussion about these at some point. If the design criteria I chose is suitable, or if we should design some others instead.
  6. Test for all functions that can now accept multivariate colormaps.
  7. Tests for Figure.colorbars()/Figure.colorbar_2D() with various placement options, norms etc.
  8. Documentation of new functionality
  9. I have attempted to write/update docstrings as I have added new functionality/classes/functions, nonetheless they probably need some refinement. Likely first by me, and then someone else.
  10. The additional functionality requires changes to a number of error messages, and new error messages will need to be raised in order to help users use the new functionality. I have attempted to make/update the most relevant cases, but more will probably be needed.
  11. Suitable names of functions/variables
  12. ...

PR checklist

Copy link

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

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

Thank you for opening your first PR into Matplotlib!

If you have not heard from us in a week or so, please leave a new comment below and that should bring it to our attention. Most of our reviewers are volunteers and sometimes things fall through the cracks.

You can also join us on gitter for real-time discussion.

For details on testing, writing docs, and our review process, please see the developer guide

We strive to be a welcoming and open project. Please follow our Code of Conduct.

@@ -1249,6 +1249,103 @@ def colorbar(
cax.figure.stale = True
return cb

def colorbars(self, mappable, shape=(-1, -1), fraction_per_row=0.15, pad=0.05,
Copy link
Member

Choose a reason for hiding this comment

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

I see this is just a placeholder, but colorbars seems too close to colorbar. Could this be multivariate_colorbars?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Agreed.
multivariate_colorbars would definitely work, or alternatively multivar_colorbars if we want it a bit shorter.

'variate' is quite a long word, and it has made sense to me to shorten it to 'var' in some cases.
However, looking at it now I think it is more important that we are consistent (which I have not really been so far, with functions such as ensure_multivariate_norm and classes like MultivarColormap).

trygvrad added 17 commits June 4, 2024 09:26
…ormap

VectorMappable cannot be an iterable because it messes up flattening of the artist tree

matplotlib.multivar_colormaps

working pcolormesh
pm = axes[0].pcolormesh((A,B,C), cmap = '3VarSubA')
works, and all tests pass

Duplicate signals are merged merged:
when calling functions in VectorMappable that causes multiple scalarmappables to update, 'changed' is now only emitted one time by VectorMappable

Added ensure_cmap() so that cmap always has a variable n_variates that can be used to evaluate the shape of the input data

also added working implementation of bivariate colormaps with square and circular clip

html representation of multivariate cmap

lint for flake8

very basic fig.colorbars() and fig.colorbar_2d()

renamed fn to ensure_multivariate_norm

error messages for multivariate colormaps

fix for resmapling of 2d colormaps (to reduce file size)

trying to get imshow to work

fixed TypeError in imshow with wrong shape

fix for test_format_cursor_data in test_image

fix for imshow with bivariate colormap
When a logNorm is used, values <= 0 are masked.
This commit lets masked arrays work with multivariate and bivaraite colormaps.
This intersects with alpha (when alpha is set), and this commit addresses this.
Added test for multivar vmin, vmax, norm and alpha.
added support for multivariate/bivaraite colormaps in pcolor
Changes to allow PatchCollection to use a bivariate/multivariate colormaps.
This is important because choropleth maps are often based on PatchCollection objects.
Cleanup to allow checking of input when clim or norm is reset on a vectormappable

tests wrong mulrivar input
cleanup errors not needed
fix for _repr_html_ for bivariate colormaps with shape='circle'
Previously, the following code would give an error.
cmap = mpl.bivar_colormaps['BiOrangeBlue']
cmap((1.0,1.0))
because calling a mpl.bivar_colormaps() assumed a 2D array.
Same issue for mpl.multivar_colormaps
trygvrad added 4 commits June 4, 2024 09:36
Previously, the shape of bivariate colormaps was only respected when they were attached to a scalarmappable, but now cm.BivarColormap.__call__() includes the code to enforce the shape ('square', 'circle', etc.)
fixes in documentation syntax

fixes to documentation syntax for correct rendering in sphinx
fix to fig.colorbar_2D() so that it works correctly with not-yet-scaled data
fix to fig.colorbar_2D() so that it works correctly with various norms (colors.LogNorm, etc.)
fix to fig.colorbars() so that it it only works with multivariate colorbars
@trygvrad trygvrad force-pushed the multivariate_colormaps_take2 branch from 92b49df to cc81df7 Compare June 4, 2024 12:18
Minor updates to .pyi files for multivariate and bivariate
@trygvrad trygvrad force-pushed the multivariate_colormaps_take2 branch from ef16916 to 945dc75 Compare June 5, 2024 08:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
Status: Needs decision
Development

Successfully merging this pull request may close these issues.

Feature request: Bivariate colormapping
3 participants