@@ -301,6 +301,8 @@ def do_3d_projection(self, renderer=None):
301301 """
302302 Project the points according to renderer matrix.
303303 """
304+ # see _update_scalarmappable docstring for why this must be here
305+ _update_scalarmappable (self )
304306 xyslist = [proj3d .proj_trans_points (points , self .axes .M )
305307 for points in self ._segments3d ]
306308 segments_2d = [np .column_stack ([xs , ys ]) for xs , ys , zs in xyslist ]
@@ -486,6 +488,8 @@ def set_3d_properties(self, zs, zdir):
486488
487489 @cbook ._delete_parameter ('3.4' , 'renderer' )
488490 def do_3d_projection (self , renderer = None ):
491+ # see _update_scalarmappable docstring for why this must be here
492+ _update_scalarmappable (self )
489493 xs , ys , zs = self ._offsets3d
490494 vxs , vys , vzs , vis = proj3d .proj_transform_clip (xs , ys , zs ,
491495 self .axes .M )
@@ -592,6 +596,8 @@ def set_linewidth(self, lw):
592596
593597 @cbook ._delete_parameter ('3.4' , 'renderer' )
594598 def do_3d_projection (self , renderer = None ):
599+ # see _update_scalarmappable docstring for why this must be here
600+ _update_scalarmappable (self )
595601 xs , ys , zs = self ._offsets3d
596602 vxs , vys , vzs , vis = proj3d .proj_transform_clip (xs , ys , zs ,
597603 self .axes .M )
@@ -635,6 +641,77 @@ def do_3d_projection(self, renderer=None):
635641 return np .min (vzs ) if vzs .size else np .nan
636642
637643
644+ def _update_scalarmappable (sm ):
645+
646+ """Update a 3D ScalarMappable.
647+
648+ With ScalarMappable objects if the data, colormap, or norm are
649+ changed, we need to update the computed colors. This is handled
650+ by the base class method update_scalarmappable. This method works
651+ by, detecting if work needs to be done, and if so stashing it on
652+ the ``self._facecolors`` attribute.
653+
654+ With 3D collections we internally sort the components so that
655+ things that should be "in front" are rendered later to simulate
656+ having a z-buffer (in addition to doing the projections). This is
657+ handled in the ``do_3d_projection`` methods which are called from the
658+ draw method of the 3D Axes. These methods:
659+
660+ - do the projection from 3D -> 2D
661+ - internally sort based on depth
662+ - stash the results of the above in the 2D analogs of state
663+ - return the z-depth of the whole artist
664+
665+ the last step is so that we can, at the Axes level, sort the children by
666+ depth.
667+
668+ The base `draw` method of the 2D artists unconditionally call
669+ update_scalarmappable and rely on the method's internal caching logic to
670+ lazily evaluate.
671+
672+ These things together mean you can have the sequence of events:
673+
674+ - we create the artist, to the color mapping and stash the results in a 3D
675+ specific state.
676+ - change something about the ScalarMappable that marks it as in need of an
677+ update (`ScalarMappable.changed` and friends).
678+ - We call do_3d_projection and shuffle the stashed colors into the 2D version
679+ of face colors
680+ - the draw method calls the update_scalarmappable method which overwrites our
681+ shuffled colors
682+ - we get a render this is wrong
683+ - if we re-render (either with a second save or implicitly via
684+ tight_layout / constairned_layout / bbox_inches='tight' (ex via
685+ inline's defaults)) we again shuffle the 3D colors
686+ - because the CM is not marked as changed update_scalarmappable is a no-op and
687+ we get a correct looking render.
688+
689+ This function is an internal helper to:
690+
691+ - sort out if we need to do the color mapping at all (has data!)
692+ - sort out if update_scalarmappable is going to be a no-op
693+ - copy the data over from the 2D -> 3D version
694+
695+ This must be called first thing in do_3d_projection to make sure that
696+ the correct colors get shuffled.
697+
698+ Parameters
699+ ----------
700+ sm : ScalarMappable
701+ The ScalarMappable to update and stash the 3D data from
702+
703+ """
704+ if sm ._A is None :
705+ return
706+ copy_state = sm ._update_dict ['array' ]
707+ ret = sm .update_scalarmappable ()
708+ if copy_state :
709+ if sm ._is_filled :
710+ sm ._facecolor3d = sm ._facecolors
711+ elif sm ._is_stroked :
712+ sm ._edgecolor3d = sm ._edgecolors
713+
714+
638715def patch_collection_2d_to_3d (col , zs = 0 , zdir = 'z' , depthshade = True ):
639716 """
640717 Convert a :class:`~matplotlib.collections.PatchCollection` into a
@@ -772,10 +849,8 @@ def do_3d_projection(self, renderer=None):
772849 """
773850 Perform the 3D projection for this object.
774851 """
775- # FIXME: This may no longer be needed?
776- if self ._A is not None :
777- self .update_scalarmappable ()
778- self ._facecolors3d = self ._facecolors
852+ # see _update_scalarmappable docstring for why this must be here
853+ _update_scalarmappable (self )
779854
780855 txs , tys , tzs = proj3d ._proj_transform_vec (self ._vec , self .axes .M )
781856 xyzlist = [(txs [sl ], tys [sl ], tzs [sl ]) for sl in self ._segslices ]
0 commit comments