Voxels as mappable for colorbar & changed return type of voxels#30982
Voxels as mappable for colorbar & changed return type of voxels#30982trygvrad wants to merge 1 commit intomatplotlib:mainfrom
Conversation
|
I think breaking the return type is a tough ask, though I'm not sure how to better structure it. No worries on refreshing the comparison images, it's not too many. Though to my eye it seems like voxels-scalar-xyz.png and voxels-scalar.png are the same? Could potentially drop one of them and compare against a single image. |
#30733 technically did that for |
|
@story645 @scottshambaugh Thank you for the feedback, please let me know what you think of the following proposition :) One possibility is to change the return type of class VoxelDict(dict):
"""
A dictionary subclass indexed by coordinate, where ``faces[i, j, k]``
is a `.Poly3DCollection` of the faces drawn for the voxel
``filled[i, j, k]``. If no faces were drawn for a given voxel,
either because it was not asked to be drawn, or it is fully
occluded, then ``(i, j, k) not in faces``.
This class also supports the functionality required to act as a mappable
for a colorbar.
"""
def __init__(self, axes, colorizer):
"""
Parameters
----------
axes : `mplot3d.axes3d.Axes3D`
The axes the voxels are contained in.
colorizer : `mpl.colorizer.Colorizer`
The colorizer uset to convert data to color.
"""
super().__init__(self)
self.axes = axes
self.colorizer = colorizer
self._callbacks = cbook.CallbackRegistry(signals=["changed"])
self._A = NoneI made a new branch with the required changes in a single commit here: main...trygvrad:matplotlib:voxels_3d_colorizer_v2 The class needs to have the following functions: def cmap(self):
def norm(self):
def callbacks(self):
def get_array(self):
def set_array(self, A):
def add_callback(self, func):
def remove_callback(self, oid):
def changed(self, *args):
def get_alpha(self):
def autoscale(self, A):
def autoscale_None(self):where we have an additional layer of callbacks to connect the voxels [ As an alternative I tried to make the new return type also inherit from class VoxelDict(dict, ColorizingArtist):
...I was able to make this work, and this simplified the class, but I observed much decreased performance. I think something was wrong with the callback. Ultimately, I found that the architecture is easier to read if the new class does not subclass from |
If we are not subclassing
It's typically not a good idea to inherit from dict. For our purposes, it should be enough to inherit from Generally, we are aspiring to move to semantic Artists that capture the logic structure of the represented data, not just a bunch of basic artists that can be used to draw the data. For example in the Voxels are in a similar situation: They can only be colored as a single collection, but are currently crated as multiple Poly3dColletions. Here, just putting them all in one Poly3dCollection is still not good enough, because that does not track the individual voxel, so a hard-coded This is just to give direction, it hasn't to be done all in one step. On an additional note, composition or inheritance can both work for the semantic Artists. |
|
Thank you @timhoffm Just a quick question, I'm not quite sure what you mean by:
They are created as multiple Poly3dColletions, but since they share a single colorizer, we can update the data either on the Poly3dColletions themselves, or via a helper function in the VoxelData type: def set_array(self, A):
"""
Set the value array from array-like *A*.
Parameters
----------
A : array-like of length equal to the number of voxels.
The values that are mapped to colors.
"""
self._A = A
for a, k in zip(A, self.keys()):
self[k].set_array(a)We could easily do the same for set_facecolor(), which would allow the syntax I'll take a look the other feedback you provided later :) |
|
My bad, I was still thinking in the ScalarMappable world where you couldn't reasonably share the colorizing config across multiple artists. |
|
Closing this as I have opened #31032 instead |
PR summary
This PR is in response to and closes #22969 [ENH]: Voxels as mappable for colorbar
This change allows axes3d.voxels() to take scalar data as input, and map it to color using a colorbar. The colors are applied to the voxels before shading.
The following code demonstrates the new functionality:
The technical implementation is such that:
filledargumentPoly3DCollectionto a singlePoly3DCollection. This change is needed so that we can callfig.colorbar()on the return value.Poly3DCollectiongets a new member function.update_scalarmappable()which overloadsCollection.update_scalarmappable(). The new member function is needed to combine the color from the colormap with the shading. This has the additional impact that we can also allow other 3d surface plots to use both cmap and shading [ax.surface(),ax.plot_trisurf()].Next step
Before we move further with this I would like to have some discussion with the maintainers if it is OK to change the return type here. I think is is really important that the user is able to call
fig.colorbar()in an easy way, but there may be other ways to achieve this goal. It is probably a good idea to discuss this at a weekly developer meeting.If we choose to move on with this I will need to add an example to the docs, a release note, and I think it would also be prudent to add more testing.
NOTE on the modification of the test images:
The change in return type change impacts how the rendering is executed, which leads to a very minor change in how PNGs are rendered [notably, no differences are observed when exporting as SVG]. This change causes all existing image-comparison tests to fail. I believe the current behaviour is a [relatively insignificant] bug, and the changes in this PR improves the rendering quality ever so slightly.
The change is illustrated using the following test image:

Rendered with 300 DPI and zoomed in:


before:
after:
I have added a line as a guide to the eye, so that the difference is visible. The discrepancy becomes greater with lower DPI, but it is also more difficult to see what is going on if the DPI is lower.
PR checklist