@@ -302,8 +302,6 @@ def do_3d_projection(self, renderer=None):
302
302
"""
303
303
Project the points according to renderer matrix.
304
304
"""
305
- # see _update_scalarmappable docstring for why this must be here
306
- _update_scalarmappable (self )
307
305
xyslist = [proj3d .proj_trans_points (points , self .axes .M )
308
306
for points in self ._segments3d ]
309
307
segments_2d = [np .column_stack ([xs , ys ]) for xs , ys , zs in xyslist ]
@@ -448,16 +446,6 @@ def set_depthshade(self, depthshade):
448
446
self ._depthshade = depthshade
449
447
self .stale = True
450
448
451
- def set_facecolor (self , c ):
452
- # docstring inherited
453
- super ().set_facecolor (c )
454
- self ._facecolor3d = self .get_facecolor ()
455
-
456
- def set_edgecolor (self , c ):
457
- # docstring inherited
458
- super ().set_edgecolor (c )
459
- self ._edgecolor3d = self .get_edgecolor ()
460
-
461
449
def set_sort_zpos (self , val ):
462
450
"""Set the position to use for z-sorting."""
463
451
self ._sort_zpos = val
@@ -474,34 +462,43 @@ def set_3d_properties(self, zs, zdir):
474
462
xs = []
475
463
ys = []
476
464
self ._offsets3d = juggle_axes (xs , ys , np .atleast_1d (zs ), zdir )
477
- self ._facecolor3d = self .get_facecolor ()
478
- self ._edgecolor3d = self .get_edgecolor ()
465
+ self ._vzs = None
479
466
self .stale = True
480
467
481
468
@_api .delete_parameter ('3.4' , 'renderer' )
482
469
def do_3d_projection (self , renderer = None ):
483
- # see _update_scalarmappable docstring for why this must be here
484
- _update_scalarmappable (self )
485
470
xs , ys , zs = self ._offsets3d
486
471
vxs , vys , vzs , vis = proj3d .proj_transform_clip (xs , ys , zs ,
487
472
self .axes .M )
488
-
489
- fcs = (_zalpha (self ._facecolor3d , vzs ) if self ._depthshade else
490
- self ._facecolor3d )
491
- fcs = mcolors .to_rgba_array (fcs , self ._alpha )
492
- super ().set_facecolor (fcs )
493
-
494
- ecs = (_zalpha (self ._edgecolor3d , vzs ) if self ._depthshade else
495
- self ._edgecolor3d )
496
- ecs = mcolors .to_rgba_array (ecs , self ._alpha )
497
- super ().set_edgecolor (ecs )
473
+ self ._vzs = vzs
498
474
super ().set_offsets (np .column_stack ([vxs , vys ]))
499
475
500
476
if vzs .size > 0 :
501
477
return min (vzs )
502
478
else :
503
479
return np .nan
504
480
481
+ def _maybe_depth_shade_and_sort_colors (self , color_array ):
482
+ color_array = (
483
+ _zalpha (color_array , self ._vzs )
484
+ if self ._vzs is not None and self ._depthshade
485
+ else color_array
486
+ )
487
+ if len (color_array ) > 1 :
488
+ color_array = color_array [self ._z_markers_idx ]
489
+ return mcolors .to_rgba_array (color_array , self ._alpha )
490
+
491
+ def get_facecolor (self ):
492
+ return self ._maybe_depth_shade_and_sort_colors (super ().get_facecolor ())
493
+
494
+ def get_edgecolor (self ):
495
+ # We need this check here to make sure we do not double-apply the depth
496
+ # based alpha shading when the edge color is "face" which means the
497
+ # edge colour should be identical to the face colour.
498
+ if cbook ._str_equal (self ._edgecolors , 'face' ):
499
+ return self .get_facecolor ()
500
+ return self ._maybe_depth_shade_and_sort_colors (super ().get_edgecolor ())
501
+
505
502
506
503
class Path3DCollection (PathCollection ):
507
504
"""
@@ -525,9 +522,14 @@ def __init__(self, *args, zs=0, zdir='z', depthshade=True, **kwargs):
525
522
This is typically desired in scatter plots.
526
523
"""
527
524
self ._depthshade = depthshade
525
+ self ._in_draw = False
528
526
super ().__init__ (* args , ** kwargs )
529
527
self .set_3d_properties (zs , zdir )
530
528
529
+ def draw (self , renderer ):
530
+ with cbook ._setattr_cm (self , _in_draw = True ):
531
+ super ().draw (renderer )
532
+
531
533
def set_sort_zpos (self , val ):
532
534
"""Set the position to use for z-sorting."""
533
535
self ._sort_zpos = val
@@ -544,12 +546,37 @@ def set_3d_properties(self, zs, zdir):
544
546
xs = []
545
547
ys = []
546
548
self ._offsets3d = juggle_axes (xs , ys , np .atleast_1d (zs ), zdir )
547
- self ._facecolor3d = self .get_facecolor ()
548
- self ._edgecolor3d = self .get_edgecolor ()
549
- self ._sizes3d = self .get_sizes ()
550
- self ._linewidth3d = self .get_linewidth ()
549
+ # In the base draw methods we access the attributes directly which
550
+ # means we can not resolve the shuffling in the getter methods like
551
+ # we do for the edge and face colors.
552
+ #
553
+ # This means we need to carry around a cache of the unsorted sizes and
554
+ # widths (postfixed with 3d) and in `do_3d_projection` set the
555
+ # depth-sorted version of that data into the private state used by the
556
+ # base collection class in its draw method.
557
+ #
558
+ # grab the current sizes and linewidths to preserve them
559
+ self ._sizes3d = self ._sizes
560
+ self ._linewidths3d = self ._linewidths
561
+ xs , ys , zs = self ._offsets3d
562
+
563
+ # Sort the points based on z coordinates
564
+ # Performance optimization: Create a sorted index array and reorder
565
+ # points and point properties according to the index array
566
+ self ._z_markers_idx = slice (- 1 )
567
+ self ._vzs = None
551
568
self .stale = True
552
569
570
+ def set_sizes (self , sizes , dpi = 72.0 ):
571
+ super ().set_sizes (sizes , dpi )
572
+ if not self ._in_draw :
573
+ self ._sizes3d = sizes
574
+
575
+ def set_linewidth (self , lw ):
576
+ super ().set_linewidth (lw )
577
+ if not self ._in_draw :
578
+ self ._linewidth3d = lw
579
+
553
580
def get_depthshade (self ):
554
581
return self ._depthshade
555
582
@@ -566,140 +593,57 @@ def set_depthshade(self, depthshade):
566
593
self ._depthshade = depthshade
567
594
self .stale = True
568
595
569
- def set_facecolor (self , c ):
570
- # docstring inherited
571
- super ().set_facecolor (c )
572
- self ._facecolor3d = self .get_facecolor ()
573
-
574
- def set_edgecolor (self , c ):
575
- # docstring inherited
576
- super ().set_edgecolor (c )
577
- self ._edgecolor3d = self .get_edgecolor ()
578
-
579
- def set_sizes (self , sizes , dpi = 72.0 ):
580
- # docstring inherited
581
- super ().set_sizes (sizes , dpi = dpi )
582
- self ._sizes3d = self .get_sizes ()
583
-
584
- def set_linewidth (self , lw ):
585
- # docstring inherited
586
- super ().set_linewidth (lw )
587
- self ._linewidth3d = self .get_linewidth ()
588
-
589
596
@_api .delete_parameter ('3.4' , 'renderer' )
590
597
def do_3d_projection (self , renderer = None ):
591
- # see _update_scalarmappable docstring for why this must be here
592
- _update_scalarmappable (self )
593
598
xs , ys , zs = self ._offsets3d
594
599
vxs , vys , vzs , vis = proj3d .proj_transform_clip (xs , ys , zs ,
595
600
self .axes .M )
596
-
597
- fcs = (_zalpha (self ._facecolor3d , vzs ) if self ._depthshade else
598
- self ._facecolor3d )
599
- ecs = (_zalpha (self ._edgecolor3d , vzs ) if self ._depthshade else
600
- self ._edgecolor3d )
601
- sizes = self ._sizes3d
602
- lws = self ._linewidth3d
603
-
604
601
# Sort the points based on z coordinates
605
602
# Performance optimization: Create a sorted index array and reorder
606
603
# points and point properties according to the index array
607
- z_markers_idx = np .argsort (vzs )[::- 1 ]
604
+ z_markers_idx = self ._z_markers_idx = np .argsort (vzs )[::- 1 ]
605
+ self ._vzs = vzs
606
+
607
+ # we have to special case the sizes because of code in collections.py
608
+ # as the draw method does
609
+ # self.set_sizes(self._sizes, self.figure.dpi)
610
+ # so we can not rely on doing the sorting on the way out via get_*
611
+
612
+ if len (self ._sizes3d ) > 1 :
613
+ self ._sizes = self ._sizes3d [z_markers_idx ]
614
+
615
+ if len (self ._linewidths3d ) > 1 :
616
+ self ._linewidths = self ._linewidths3d [z_markers_idx ]
608
617
609
618
# Re-order items
610
619
vzs = vzs [z_markers_idx ]
611
620
vxs = vxs [z_markers_idx ]
612
621
vys = vys [z_markers_idx ]
613
- if len (fcs ) > 1 :
614
- fcs = fcs [z_markers_idx ]
615
- if len (ecs ) > 1 :
616
- ecs = ecs [z_markers_idx ]
617
- if len (sizes ) > 1 :
618
- sizes = sizes [z_markers_idx ]
619
- if len (lws ) > 1 :
620
- lws = lws [z_markers_idx ]
621
- vps = np .column_stack ((vxs , vys ))
622
-
623
- fcs = mcolors .to_rgba_array (fcs , self ._alpha )
624
- ecs = mcolors .to_rgba_array (ecs , self ._alpha )
625
-
626
- super ().set_edgecolor (ecs )
627
- super ().set_facecolor (fcs )
628
- super ().set_sizes (sizes )
629
- super ().set_linewidth (lws )
630
-
631
- PathCollection .set_offsets (self , vps )
632
622
633
- return np . min ( vzs ) if vzs . size else np .nan
623
+ PathCollection . set_offsets ( self , np .column_stack (( vxs , vys )))
634
624
625
+ return np .min (vzs ) if vzs .size else np .nan
635
626
636
- def _update_scalarmappable (sm ):
637
- """
638
- Update a 3D ScalarMappable.
639
-
640
- With ScalarMappable objects if the data, colormap, or norm are
641
- changed, we need to update the computed colors. This is handled
642
- by the base class method update_scalarmappable. This method works
643
- by detecting if work needs to be done, and if so stashing it on
644
- the ``self._facecolors`` attribute.
645
-
646
- With 3D collections we internally sort the components so that
647
- things that should be "in front" are rendered later to simulate
648
- having a z-buffer (in addition to doing the projections). This is
649
- handled in the ``do_3d_projection`` methods which are called from the
650
- draw method of the 3D Axes. These methods:
651
-
652
- - do the projection from 3D -> 2D
653
- - internally sort based on depth
654
- - stash the results of the above in the 2D analogs of state
655
- - return the z-depth of the whole artist
656
-
657
- the last step is so that we can, at the Axes level, sort the children by
658
- depth.
659
-
660
- The base `draw` method of the 2D artists unconditionally calls
661
- update_scalarmappable and rely on the method's internal caching logic to
662
- lazily evaluate.
663
-
664
- These things together mean you can have the sequence of events:
665
-
666
- - we create the artist, do the color mapping and stash the results
667
- in a 3D specific state.
668
- - change something about the ScalarMappable that marks it as in
669
- need of an update (`ScalarMappable.changed` and friends).
670
- - We call do_3d_projection and shuffle the stashed colors into the
671
- 2D version of face colors
672
- - the draw method calls the update_scalarmappable method which
673
- overwrites our shuffled colors
674
- - we get a render that is wrong
675
- - if we re-render (either with a second save or implicitly via
676
- tight_layout / constrained_layout / bbox_inches='tight' (ex via
677
- inline's defaults)) we again shuffle the 3D colors
678
- - because the CM is not marked as changed update_scalarmappable is
679
- a no-op and we get a correct looking render.
680
-
681
- This function is an internal helper to:
682
-
683
- - sort out if we need to do the color mapping at all (has data!)
684
- - sort out if update_scalarmappable is going to be a no-op
685
- - copy the data over from the 2D -> 3D version
686
-
687
- This must be called first thing in do_3d_projection to make sure that
688
- the correct colors get shuffled.
627
+ def _maybe_depth_shade_and_sort_colors (self , color_array ):
628
+ color_array = (
629
+ _zalpha (color_array , self ._vzs )
630
+ if self ._vzs is not None and self ._depthshade
631
+ else color_array
632
+ )
633
+ if len (color_array ) > 1 :
634
+ color_array = color_array [self ._z_markers_idx ]
635
+ return mcolors .to_rgba_array (color_array , self ._alpha )
689
636
690
- Parameters
691
- ----------
692
- sm : ScalarMappable
693
- The ScalarMappable to update and stash the 3D data from
637
+ def get_facecolor (self ):
638
+ return self ._maybe_depth_shade_and_sort_colors (super ().get_facecolor ())
694
639
695
- """
696
- if sm ._A is None :
697
- return
698
- sm .update_scalarmappable ()
699
- if sm ._face_is_mapped :
700
- sm ._facecolor3d = sm ._facecolors
701
- elif sm ._edge_is_mapped : # Should this be plain "if"?
702
- sm ._edgecolor3d = sm ._edgecolors
640
+ def get_edgecolor (self ):
641
+ # We need this check here to make sure we do not double-apply the depth
642
+ # based alpha shading when the edge color is "face" which means the
643
+ # edge colour should be identical to the face colour.
644
+ if cbook ._str_equal (self ._edgecolors , 'face' ):
645
+ return self .get_facecolor ()
646
+ return self ._maybe_depth_shade_and_sort_colors (super ().get_edgecolor ())
703
647
704
648
705
649
def patch_collection_2d_to_3d (col , zs = 0 , zdir = 'z' , depthshade = True ):
@@ -725,6 +669,7 @@ def patch_collection_2d_to_3d(col, zs=0, zdir='z', depthshade=True):
725
669
elif isinstance (col , PatchCollection ):
726
670
col .__class__ = Patch3DCollection
727
671
col ._depthshade = depthshade
672
+ col ._in_draw = False
728
673
col .set_3d_properties (zs , zdir )
729
674
730
675
@@ -839,9 +784,19 @@ def do_3d_projection(self, renderer=None):
839
784
"""
840
785
Perform the 3D projection for this object.
841
786
"""
842
- # see _update_scalarmappable docstring for why this must be here
843
- _update_scalarmappable (self )
844
-
787
+ if self ._A is not None :
788
+ # force update of color mapping because we re-order them
789
+ # below. If we do not do this here, the 2D draw will call
790
+ # this, but we will never port the color mapped values back
791
+ # to the 3D versions.
792
+ #
793
+ # We hold the 3D versions in a fixed order (the order the user
794
+ # passed in) and sort the 2D version by view depth.
795
+ self .update_scalarmappable ()
796
+ if self ._face_is_mapped :
797
+ self ._facecolor3d = self ._facecolors
798
+ if self ._edge_is_mapped :
799
+ self ._edgecolor3d = self ._edgecolors
845
800
txs , tys , tzs = proj3d ._proj_transform_vec (self ._vec , self .axes .M )
846
801
xyzlist = [(txs [sl ], tys [sl ], tzs [sl ]) for sl in self ._segslices ]
847
802
0 commit comments