@@ -264,6 +264,8 @@ def do_3d_projection(self, renderer):
264
264
"""
265
265
Project the points according to renderer matrix.
266
266
"""
267
+ # see _update_scalarmappable docstring for why this must be here
268
+ _update_scalarmappable (self )
267
269
xyslist = [
268
270
proj3d .proj_trans_points (points , renderer .M ) for points in
269
271
self ._segments3d ]
@@ -418,6 +420,8 @@ def set_3d_properties(self, zs, zdir):
418
420
self .stale = True
419
421
420
422
def do_3d_projection (self , renderer ):
423
+ # see _update_scalarmappable docstring for why this must be here
424
+ _update_scalarmappable (self )
421
425
xs , ys , zs = self ._offsets3d
422
426
vxs , vys , vzs , vis = proj3d .proj_transform_clip (xs , ys , zs , renderer .M )
423
427
@@ -486,6 +490,8 @@ def set_3d_properties(self, zs, zdir):
486
490
self .stale = True
487
491
488
492
def do_3d_projection (self , renderer ):
493
+ # see _update_scalarmappable docstring for why this must be here
494
+ _update_scalarmappable (self )
489
495
xs , ys , zs = self ._offsets3d
490
496
vxs , vys , vzs , vis = proj3d .proj_transform_clip (xs , ys , zs , renderer .M )
491
497
@@ -528,6 +534,77 @@ def do_3d_projection(self, renderer):
528
534
return np .min (vzs ) if vzs .size else np .nan
529
535
530
536
537
+ def _update_scalarmappable (sm ):
538
+ """
539
+ Update a 3D ScalarMappable.
540
+
541
+ With ScalarMappable objects if the data, colormap, or norm are
542
+ changed, we need to update the computed colors. This is handled
543
+ by the base class method update_scalarmappable. This method works
544
+ by, detecting if work needs to be done, and if so stashing it on
545
+ the ``self._facecolors`` attribute.
546
+
547
+ With 3D collections we internally sort the components so that
548
+ things that should be "in front" are rendered later to simulate
549
+ having a z-buffer (in addition to doing the projections). This is
550
+ handled in the ``do_3d_projection`` methods which are called from the
551
+ draw method of the 3D Axes. These methods:
552
+
553
+ - do the projection from 3D -> 2D
554
+ - internally sort based on depth
555
+ - stash the results of the above in the 2D analogs of state
556
+ - return the z-depth of the whole artist
557
+
558
+ the last step is so that we can, at the Axes level, sort the children by
559
+ depth.
560
+
561
+ The base `draw` method of the 2D artists unconditionally calls
562
+ update_scalarmappable and rely on the method's internal caching logic to
563
+ lazily evaluate.
564
+
565
+ These things together mean you can have the sequence of events:
566
+
567
+ - we create the artist, do the color mapping and stash the results
568
+ in a 3D specific state.
569
+ - change something about the ScalarMappable that marks it as in
570
+ need of an update (`ScalarMappable.changed` and friends).
571
+ - We call do_3d_projection and shuffle the stashed colors into the
572
+ 2D version of face colors
573
+ - the draw method calls the update_scalarmappable method which
574
+ overwrites our shuffled colors
575
+ - we get a render that is wrong
576
+ - if we re-render (either with a second save or implicitly via
577
+ tight_layout / constrained_layout / bbox_inches='tight' (ex via
578
+ inline's defaults)) we again shuffle the 3D colors
579
+ - because the CM is not marked as changed update_scalarmappable is
580
+ a no-op and we get a correct looking render.
581
+
582
+ This function is an internal helper to:
583
+
584
+ - sort out if we need to do the color mapping at all (has data!)
585
+ - sort out if update_scalarmappable is going to be a no-op
586
+ - copy the data over from the 2D -> 3D version
587
+
588
+ This must be called first thing in do_3d_projection to make sure that
589
+ the correct colors get shuffled.
590
+
591
+ Parameters
592
+ ----------
593
+ sm : ScalarMappable
594
+ The ScalarMappable to update and stash the 3D data from
595
+
596
+ """
597
+ if sm ._A is None :
598
+ return
599
+ copy_state = sm ._update_dict ['array' ]
600
+ ret = sm .update_scalarmappable ()
601
+ if copy_state :
602
+ if sm ._is_filled :
603
+ sm ._facecolor3d = sm ._facecolors
604
+ elif sm ._is_stroked :
605
+ sm ._edgecolor3d = sm ._edgecolors
606
+
607
+
531
608
def patch_collection_2d_to_3d (col , zs = 0 , zdir = 'z' , depthshade = True ):
532
609
"""
533
610
Convert a :class:`~matplotlib.collections.PatchCollection` into a
@@ -650,8 +727,8 @@ def set_3d_properties(self):
650
727
self .update_scalarmappable ()
651
728
self ._sort_zpos = None
652
729
self .set_zsort ('average' )
653
- self ._facecolors3d = PolyCollection .get_facecolor (self )
654
- self ._edgecolors3d = PolyCollection .get_edgecolor (self )
730
+ self ._facecolor3d = PolyCollection .get_facecolor (self )
731
+ self ._edgecolor3d = PolyCollection .get_edgecolor (self )
655
732
self ._alpha3d = PolyCollection .get_alpha (self )
656
733
self .stale = True
657
734
@@ -664,17 +741,15 @@ def do_3d_projection(self, renderer):
664
741
"""
665
742
Perform the 3D projection for this object.
666
743
"""
667
- # FIXME: This may no longer be needed?
668
- if self ._A is not None :
669
- self .update_scalarmappable ()
670
- self ._facecolors3d = self ._facecolors
744
+ # see _update_scalarmappable docstring for why this must be here
745
+ _update_scalarmappable (self )
671
746
672
747
txs , tys , tzs = proj3d ._proj_transform_vec (self ._vec , renderer .M )
673
748
xyzlist = [(txs [sl ], tys [sl ], tzs [sl ]) for sl in self ._segslices ]
674
749
675
750
# This extra fuss is to re-order face / edge colors
676
- cface = self ._facecolors3d
677
- cedge = self ._edgecolors3d
751
+ cface = self ._facecolor3d
752
+ cedge = self ._edgecolor3d
678
753
if len (cface ) != len (xyzlist ):
679
754
cface = cface .repeat (len (xyzlist ), axis = 0 )
680
755
if len (cedge ) != len (xyzlist ):
@@ -699,8 +774,8 @@ def do_3d_projection(self, renderer):
699
774
else :
700
775
PolyCollection .set_verts (self , segments_2d , self ._closed )
701
776
702
- if len (self ._edgecolors3d ) != len (cface ):
703
- self ._edgecolors2d = self ._edgecolors3d
777
+ if len (self ._edgecolor3d ) != len (cface ):
778
+ self ._edgecolors2d = self ._edgecolor3d
704
779
705
780
# Return zorder value
706
781
if self ._sort_zpos is not None :
@@ -717,23 +792,23 @@ def do_3d_projection(self, renderer):
717
792
718
793
def set_facecolor (self , colors ):
719
794
PolyCollection .set_facecolor (self , colors )
720
- self ._facecolors3d = PolyCollection .get_facecolor (self )
795
+ self ._facecolor3d = PolyCollection .get_facecolor (self )
721
796
722
797
def set_edgecolor (self , colors ):
723
798
PolyCollection .set_edgecolor (self , colors )
724
- self ._edgecolors3d = PolyCollection .get_edgecolor (self )
799
+ self ._edgecolor3d = PolyCollection .get_edgecolor (self )
725
800
726
801
def set_alpha (self , alpha ):
727
802
# docstring inherited
728
803
artist .Artist .set_alpha (self , alpha )
729
804
try :
730
- self ._facecolors3d = mcolors .to_rgba_array (
731
- self ._facecolors3d , self ._alpha )
805
+ self ._facecolor3d = mcolors .to_rgba_array (
806
+ self ._facecolor3d , self ._alpha )
732
807
except (AttributeError , TypeError , IndexError ):
733
808
pass
734
809
try :
735
810
self ._edgecolors = mcolors .to_rgba_array (
736
- self ._edgecolors3d , self ._alpha )
811
+ self ._edgecolor3d , self ._alpha )
737
812
except (AttributeError , TypeError , IndexError ):
738
813
pass
739
814
self .stale = True
0 commit comments