FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

[ENH]: Voxels as mappable for colorbar · Issue #22969 · matplotlib/matplotlib · GitHub

Repository navigation

[ENH]: Voxels as mappable for colorbar #22969

Description

Problem

Hi;

I'm working with 3D plots and I use colored voxels to plots a 4th dimension.

When one wants to use a colorbar, one usually uses an image or a contourf in the colorbar creation method like so :

img = ax.imshow(event, **cmap_args)
plt.colorbar(img)

It is very handful to handle all the colormap arguments (such as vmin etc) directly from the image but doesn't work with voxels.

Currently I use this :

`

palette=mpl.cm.get_cmap("viridis",lut=levels).copy()     #simulate the level arg of other plot methods
palette.set_under((0,0,0,0))                             #So vmin has an effect
if add_colorbar:

vmax = np.max(NPArr)                                    #NPArr is the data being plotted
norm = mpl.colors.Normalize(0,vmax)                     #Necessary because there is no mappable from voxels, instead of normalizing the data it is easier to normalize the "cmap"
m = cm.ScalarMappable(cmap=palette,norm=norm)           #Creates Mappable
m.set_array([])                                         #Makes it the right type so get() methods exist
ticks = np.linspace(1,vmax,levels+1)
fig.colorbar(m, ax=ax, extend="min", ticks=ticks)       #Finally creates the cmap with other cmap arguments that can't be passed to the mappable

`

Voxels give dictionnary of 3d objet so I have tried to use one of its value as mappable which works but doesn't bring all the data info for the colormap.

Proposed solution

No response

Activity

  1. jklymak commented on May 4, 2022

    Member

    Voxels doesn't do colormapping so how are we to infer the colormap map from the artist? Please provide a complete example, but this doesn't seem possible.

  2. tacaswell commented on May 4, 2022

    Member

    It seems reasonable that we "promote" the artists used in Voxel to be a scalar mappable.

  3. timhoffm commented on May 5, 2022

    Member

    The API extension would be straight forward: ax.voxels([x, y, z,] /, filled, ...

      filled : 3D np.array of bool
          A 3D array of values, with truthy values indicating which voxels
          to fill
    

    alternatively to bool, filled could accepts numeric values to be mapped (with NaN meaning unfilled).

    The main task however is to create a new VoxelCollection Artist. Currently we return a dict of Poly3DCollection, but we need one container Artist that derives from ScalarMappable and can carry the numeric and colormapping information.

  4. added
    Difficulty: Mediumhttps://matplotlib.org/devdocs/devel/contribute.html#good-first-issues
    and removed on May 5, 2022
  5. Luluser commented on May 6, 2022

    Author

    Voxels doesn't do colormapping so how are we to infer the colormap map from the artist? Please provide a complete example, but this doesn't seem possible.

    I'm not sure if you need a working exmaple anymore but here it is :
    let's say you have 4D data ie a value for every point in 3D and that is is sparse enough not to make voxels in the whole 3D space.
    My personal case is a Histogram ie a at every point in the 3D space you have the number of occurence of your three simultaenous axes variables most of it value being 0.
    The 3 axes variables binned and the 3D histogram :

     #Mock data
    
    X,Y,Z = np.meshgrid(np.linspace(0,5,10),np.linspace(0,5,10),np.linspace(0,5,10),indexing="ij")
    Hist=np.zeros((9,9,9))
    Hist[2,5,5],Hist[5,5,3],Hist[5,7,5],Hist[5,0,5]=1,55,4,39
    

    To create the BoolArray for the voxels method I use a mask on those the minimal value I want to display (let's say 0 for simplicity):

    BoolArr=(Hist > 0)
    

    To color the voxels by the value in the histogram I use a colormap on the normalized data :

    CArr = cmap(NPArr/np.max(NPArr))
    CArr[:,:,:,3]=0.9
    

    The slight transparency will make the plot more readable. I now have a color for every point in space, since the BoolArray will provide the actual voxels that need to be plotted I don't have to mask this ColorArray.

    I can now call :

    fig=plt.figure()
    ax = fig.add_subplot(projection='3d')
    ax.voxels(X, Y, Z, BoolArr, facecolors=CArr) 
    

    which makes the plot I'm looking for.
    However for the cbar it is a bit more complicated,

    Here is the whole code adapted from the one I currently use :
    `
    import numpy as np
    import matplotlib as mpl
    import matplotlib.pyplot as plt

    levels=8
    cmap="viridis"
    
    #Mock data
    X,Y,Z = np.meshgrid(np.linspace(0,5,10),np.linspace(0,5,10),np.linspace(0,5,10),indexing="ij")
    Hist=np.zeros((9,9,9))
    Hist[2,5,5],Hist[5,5,3],Hist[5,7,5],Hist[5,0,5]=1,55,4,39
    
    
    palette=mpl.cm.get_cmap("viridis",lut=levels).copy()     #simulate the level arg of other plot methods
    palette.set_under((0,0,0,0))                             #So vmin=0 has an effect
    
    BoolArr=(Hist > 0)
    vmax=np.max(Hist)
    CArr = palette(Hist/vmax)
    print(np.shape(CArr))
    CArr[:,:,:,3]=0.9
    
    fig=plt.figure()
    ax = fig.add_subplot(projection='3d')
    
    primitive = ax.voxels(X, Y, Z, BoolArr, facecolors=CArr)
    
    #Now we have to create the cbar
    
    
    norm = mpl.colors.Normalize(0,vmax)                     #Necessary because there is no mappable from voxels, instead of normalizing the data it is easier to normalize the "cmap"
    m = mpl.cm.ScalarMappable(cmap=palette,norm=norm)       #Creates Mappable
    m.set_array([])                                         #Makes it the right type so get() methods exist
    ticks = np.linspace(1,vmax,levels+1)
    fig.colorbar(m, ax=ax, extend="min", ticks=ticks)       #Finally creates the cmap with other cmap arguments that can't be passed to the mappable
    
    plt.show()
    

    `

    So voxels would need a new argument "value" and that we can pass on a cmap so thats it store internally the data to give the right mappable. Or perhaps something less complicated would be that the BoolArray could actually be an array of float and that the arguments of vmin and vmax would control which values to be displayed by masking them out. The Nan values would replace the False values so we could imagine a FloatArray anyway where Nan values would be ignored in the plotting it would makes sense in the plotting of datasets too !

    I hope it helps

  6. Luluser commented on May 6, 2022

    Author

    The API extension would be straight forward: ax.voxels([x, y, z,] /, filled, ...

      filled : 3D np.array of bool
          A 3D array of values, with truthy values indicating which voxels
          to fill
    

    alternatively to bool, filled could accepts numeric values to be mapped (with NaN meaning unfilled).

    The main task however is to create a new VoxelCollection Artist. Currently we return a dict of Poly3DCollection, but we need one container Artist that derives from ScalarMappable and can carry the numeric and colormapping information.

    Oh ! That's exactly what I had in mind. I have no idea if my request is actually tough though, I don't know much of this part of the API.

  7. greglucas commented on May 6, 2022

    Contributor

    @Luluser, in case it is of interest, we are having a new contributors meeting this next week. We could walk through this section of the API then if that would be helpful at all. https://discourse.matplotlib.org/t/matplotlib-devel-announcing-the-new-contributors-meeting/22808

  8. jklymak commented on May 6, 2022

    Member

    The main task however is to create a new VoxelCollection Artist

    I think that needs to be a different axes method?

    But regardless, currently you are doing the mapping manually so it makes sense that you need to create the colorbar manually.

  9. github-actions commented on Dec 5, 2025

    This issue has been marked "inactive" because it has been 365 days since the last comment. If this issue is still present in recent Matplotlib releases, or the feature request is still wanted, please leave a comment and this label will be removed. If there are no updates in another 30 days, this issue will be automatically closed, but you are free to re-open or create a new issue if needed. We value issue reports, and this procedure is meant to help us resurface and prioritize issues that have not been addressed yet, not make them disappear. Thanks for your help!

  10. added
    keepItems to be ignored by the “Stale” Github Action
    and removed
    status: inactiveMarked by the “Stale” Github Action
    on Dec 5, 2025
  11. story645 commented on Dec 5, 2025

    Member

    @trygvrad would this be easier to implement w/ the colorizer architecture?

  12. trygvrad commented on Dec 6, 2025

    Contributor

    would this be easier to implement w/ the colorizer architecture?

    Yes, I believe it is.

    From what I can see, ax.voxels() returns a dictionary of Poly3DCollection, which inherits:
    Poly3DCollection ← PolyCollection ← _CollectionWithSizes ← Collection ← ColorizingArtist

    This means that there is already a Colorizer object associated with each voxel as it is rendered, because the creation of a ColorizingArtist instantiates a Colorizer if one is not provided as a keyword argument.

    If we include a colorizer=colorizer to the initialization of each Poly3DCollection, it is trivial to have a single colorizer that is responsible for all voxels.


    I made a branch that implements the required changes with as single commit

    with this you are able to do:

    import numpy as np
    import matplotlib.pyplot as plt
    
    #Mock data
    X,Y,Z = np.meshgrid(np.linspace(0,5,10), np.linspace(0,5,10), np.linspace(0,5,10), indexing="ij")
    Hist = np.zeros((9,9,9))
    Hist[2,5,5], Hist[5,5,3], Hist[5,7,5], Hist[5,0,5] = 1, 55, 4, 39
    BoolArr = (Hist > 0)
    
    # plot
    fig=plt.figure()
    ax = fig.add_subplot(projection='3d')
    res = ax.voxels(X, Y, Z, BoolArr, facecolors=Hist, cmap='rainbow', norm='log', alpha=0.5)
    fig.colorbar(list(res.values())[0])

    For the implementation I let the data be input via the facecolors argument:

        facecolors, edgecolors : array-like, optional
            The color to draw the faces and edges of the voxels. Can only be
            passed as keyword arguments.
            These parameters can be:
    
            - A single color value, to color all voxels the same color. This
              can be either a string, or a 1D RGB/RGBA array
            - ``None``, the default, to use a single color for the faces, and
              the style default for the edges.
            - A 3D `~numpy.ndarray` of color names, with each item the color
              for the corresponding voxel. The size must match the voxels.
            - facecolors only: A 3D `~numpy.ndarray` of scalar values, with 
              each value mapped using a norm and colormap.
            - A 4D `~numpy.ndarray` of RGB/RGBA data, with the components
              along the last axis.
    

    This makes the most sense to me, but we could alternatively add an additional keyword argument, as has been suggested previously.


    The line

    fig.colorbar(list(res.values())[0])

    is not very elegant. It arises because of the return type of ax.voxels(), which is a dictionary.
    We can fix this by creating a new class as the return type for ax.voxels(), but I am not sure if this is strictly necessary.


    @story645 what do you think?
    If you think this is a reasonable solution I can add tests to my branch and make a PR.

  13. story645 commented on Dec 8, 2025

    Member

    If you think this is a reasonable solution I can add tests to my branch and make a PR.

    Thanks for the quick workup. I'm not sure about passing in via facecolors b/c then in theory folks may also want to do edgecolors - I like @timhoffm's suggestion of using filled #22969 (comment)?

    We can fix this by creating a new class as the return type for ax.voxels(), but I am not sure if this is strictly necessary.

    If it doesn't get too cranky, I think a VoxelCollection artist would make sense for a couple of reasons, and we've been introducing more semantic artists lately.

    But that can also all be discussed on the PR.

  14. trygvrad commented on Dec 9, 2025

    Contributor

    hmmm, I can take a crack at seeing what would be required of a VoxelCollection,

    Will VoxelCollection contain a dictionary of Poly3DCollection objects, i.e. an artist containing other artists, or do we think that we can make the VoxelCollection the only artist?

    I'm not familiar enough with the 3D pipeline to know what is needed for the objects to be ordered correctly in screen depth, and if this has any impact of single/multiple artists.

    (are there other kinds of artists that contain artists?)

  15. WeatherGod commented on Dec 9, 2025

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    Difficulty: Mediumhttps://matplotlib.org/devdocs/devel/contribute.html#good-first-issuesNew featurekeepItems to be ignored by the “Stale” Github Actiontopic: mplot3d

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions


      Back | FazBrowse Home | New Git URL