@@ -301,6 +301,8 @@ def do_3d_projection(self, renderer=None):
301
301
"""
302
302
Project the points according to renderer matrix.
303
303
"""
304
+ # see _update_scalarmappable docstring for why this must be here
305
+ _update_scalarmappable (self )
304
306
xyslist = [proj3d .proj_trans_points (points , self .axes .M )
305
307
for points in self ._segments3d ]
306
308
segments_2d = [np .column_stack ([xs , ys ]) for xs , ys , zs in xyslist ]
@@ -486,6 +488,8 @@ def set_3d_properties(self, zs, zdir):
486
488
487
489
@cbook ._delete_parameter ('3.4' , 'renderer' )
488
490
def do_3d_projection (self , renderer = None ):
491
+ # see _update_scalarmappable docstring for why this must be here
492
+ _update_scalarmappable (self )
489
493
xs , ys , zs = self ._offsets3d
490
494
vxs , vys , vzs , vis = proj3d .proj_transform_clip (xs , ys , zs ,
491
495
self .axes .M )
@@ -592,6 +596,8 @@ def set_linewidth(self, lw):
592
596
593
597
@cbook ._delete_parameter ('3.4' , 'renderer' )
594
598
def do_3d_projection (self , renderer = None ):
599
+ # see _update_scalarmappable docstring for why this must be here
600
+ _update_scalarmappable (self )
595
601
xs , ys , zs = self ._offsets3d
596
602
vxs , vys , vzs , vis = proj3d .proj_transform_clip (xs , ys , zs ,
597
603
self .axes .M )
@@ -635,6 +641,77 @@ def do_3d_projection(self, renderer=None):
635
641
return np .min (vzs ) if vzs .size else np .nan
636
642
637
643
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 calls
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, do the color mapping and stash the results
675
+ in a 3D specific state.
676
+ - change something about the ScalarMappable that marks it as in
677
+ need of an update (`ScalarMappable.changed` and friends).
678
+ - We call do_3d_projection and shuffle the stashed colors into the
679
+ 2D version of face colors
680
+ - the draw method calls the update_scalarmappable method which
681
+ overwrites our shuffled colors
682
+ - we get a render that is wrong
683
+ - if we re-render (either with a second save or implicitly via
684
+ tight_layout / constrained_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
687
+ a no-op and 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
+
638
715
def patch_collection_2d_to_3d (col , zs = 0 , zdir = 'z' , depthshade = True ):
639
716
"""
640
717
Convert a :class:`~matplotlib.collections.PatchCollection` into a
@@ -757,8 +834,8 @@ def set_3d_properties(self):
757
834
self .update_scalarmappable ()
758
835
self ._sort_zpos = None
759
836
self .set_zsort ('average' )
760
- self ._facecolors3d = PolyCollection .get_facecolor (self )
761
- self ._edgecolors3d = PolyCollection .get_edgecolor (self )
837
+ self ._facecolor3d = PolyCollection .get_facecolor (self )
838
+ self ._edgecolor3d = PolyCollection .get_edgecolor (self )
762
839
self ._alpha3d = PolyCollection .get_alpha (self )
763
840
self .stale = True
764
841
@@ -772,17 +849,15 @@ def do_3d_projection(self, renderer=None):
772
849
"""
773
850
Perform the 3D projection for this object.
774
851
"""
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 )
779
854
780
855
txs , tys , tzs = proj3d ._proj_transform_vec (self ._vec , self .axes .M )
781
856
xyzlist = [(txs [sl ], tys [sl ], tzs [sl ]) for sl in self ._segslices ]
782
857
783
858
# This extra fuss is to re-order face / edge colors
784
- cface = self ._facecolors3d
785
- cedge = self ._edgecolors3d
859
+ cface = self ._facecolor3d
860
+ cedge = self ._edgecolor3d
786
861
if len (cface ) != len (xyzlist ):
787
862
cface = cface .repeat (len (xyzlist ), axis = 0 )
788
863
if len (cedge ) != len (xyzlist ):
@@ -807,8 +882,8 @@ def do_3d_projection(self, renderer=None):
807
882
else :
808
883
PolyCollection .set_verts (self , segments_2d , self ._closed )
809
884
810
- if len (self ._edgecolors3d ) != len (cface ):
811
- self ._edgecolors2d = self ._edgecolors3d
885
+ if len (self ._edgecolor3d ) != len (cface ):
886
+ self ._edgecolors2d = self ._edgecolor3d
812
887
813
888
# Return zorder value
814
889
if self ._sort_zpos is not None :
@@ -826,24 +901,24 @@ def do_3d_projection(self, renderer=None):
826
901
def set_facecolor (self , colors ):
827
902
# docstring inherited
828
903
super ().set_facecolor (colors )
829
- self ._facecolors3d = PolyCollection .get_facecolor (self )
904
+ self ._facecolor3d = PolyCollection .get_facecolor (self )
830
905
831
906
def set_edgecolor (self , colors ):
832
907
# docstring inherited
833
908
super ().set_edgecolor (colors )
834
- self ._edgecolors3d = PolyCollection .get_edgecolor (self )
909
+ self ._edgecolor3d = PolyCollection .get_edgecolor (self )
835
910
836
911
def set_alpha (self , alpha ):
837
912
# docstring inherited
838
913
artist .Artist .set_alpha (self , alpha )
839
914
try :
840
- self ._facecolors3d = mcolors .to_rgba_array (
841
- self ._facecolors3d , self ._alpha )
915
+ self ._facecolor3d = mcolors .to_rgba_array (
916
+ self ._facecolor3d , self ._alpha )
842
917
except (AttributeError , TypeError , IndexError ):
843
918
pass
844
919
try :
845
920
self ._edgecolors = mcolors .to_rgba_array (
846
- self ._edgecolors3d , self ._alpha )
921
+ self ._edgecolor3d , self ._alpha )
847
922
except (AttributeError , TypeError , IndexError ):
848
923
pass
849
924
self .stale = True
0 commit comments