Skip to content

Commit 14ecfcc

Browse files
authored
Merge pull request #18480 from efiring/pcolor_edges
Clarify color priorities in collections
2 parents 01d3149 + 4c3d644 commit 14ecfcc

File tree

7 files changed

+262
-100
lines changed

7 files changed

+262
-100
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
Collection color specification and mapping
2+
------------------------------------------
3+
4+
Reworking the handling of color mapping and the keyword arguments for facecolor
5+
and edgecolor has resulted in three behavior changes:
6+
7+
1. Color mapping can be turned off by calling ``Collection.set_array(None)``.
8+
Previously, this would have no effect.
9+
2. When a mappable array is set, with ``facecolor='none'`` and
10+
``edgecolor='face'``, both the faces and the edges are left uncolored.
11+
Previously the edges would be color-mapped.
12+
3. When a mappable array is set, with ``facecolor='none'`` and
13+
``edgecolor='red'``, the edges are red. This addresses Issue #1302.
14+
Previously the edges would be color-mapped.

lib/matplotlib/axes/_axes.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -6154,7 +6154,7 @@ def pcolormesh(self, *args, alpha=None, norm=None, cmap=None, vmin=None,
61546154
if shading is None:
61556155
shading = rcParams['pcolor.shading']
61566156
shading = shading.lower()
6157-
kwargs.setdefault('edgecolors', 'None')
6157+
kwargs.setdefault('edgecolors', 'none')
61586158

61596159
X, Y, C, shading = self._pcolorargs('pcolormesh', *args,
61606160
shading=shading, kwargs=kwargs)

lib/matplotlib/cm.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -366,7 +366,7 @@ def set_array(self, A):
366366
367367
Parameters
368368
----------
369-
A : ndarray
369+
A : ndarray or None
370370
"""
371371
self._A = A
372372
self._update_dict['array'] = True

lib/matplotlib/collections.py

+145-91
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020
import warnings
2121

2222

23+
# "color" is excluded; it is a compound setter, and its docstring differs
24+
# in LineCollection.
2325
@cbook._define_aliases({
2426
"antialiased": ["antialiaseds", "aa"],
2527
"edgecolor": ["edgecolors", "ec"],
@@ -168,8 +170,10 @@ def __init__(self,
168170
# list of unbroadcast/scaled linewidths
169171
self._us_lw = [0]
170172
self._linewidths = [0]
171-
self._is_filled = True # May be modified by set_facecolor().
172-
173+
# Flags set by _set_mappable_flags: are colors from mapping an array?
174+
self._face_is_mapped = None
175+
self._edge_is_mapped = None
176+
self._mapped_colors = None # calculated in update_scalarmappable
173177
self._hatch_color = mcolors.to_rgba(mpl.rcParams['hatch.color'])
174178
self.set_facecolor(facecolors)
175179
self.set_edgecolor(edgecolors)
@@ -586,6 +590,10 @@ def get_offset_position(self):
586590
"""
587591
return self._offset_position
588592

593+
def _get_default_linewidth(self):
594+
# This may be overridden in a subclass.
595+
return mpl.rcParams['patch.linewidth'] # validated as float
596+
589597
def set_linewidth(self, lw):
590598
"""
591599
Set the linewidth(s) for the collection. *lw* can be a scalar
@@ -597,9 +605,7 @@ def set_linewidth(self, lw):
597605
lw : float or list of floats
598606
"""
599607
if lw is None:
600-
lw = mpl.rcParams['patch.linewidth']
601-
if lw is None:
602-
lw = mpl.rcParams['lines.linewidth']
608+
lw = self._get_default_linewidth()
603609
# get the un-scaled/broadcast lw
604610
self._us_lw = np.atleast_1d(np.asarray(lw))
605611

@@ -730,10 +736,14 @@ def set_antialiased(self, aa):
730736
aa : bool or list of bools
731737
"""
732738
if aa is None:
733-
aa = mpl.rcParams['patch.antialiased']
739+
aa = self._get_default_antialiased()
734740
self._antialiaseds = np.atleast_1d(np.asarray(aa, bool))
735741
self.stale = True
736742

743+
def _get_default_antialiased(self):
744+
# This may be overridden in a subclass.
745+
return mpl.rcParams['patch.antialiased']
746+
737747
def set_color(self, c):
738748
"""
739749
Set both the edgecolor and the facecolor.
@@ -750,16 +760,14 @@ def set_color(self, c):
750760
self.set_facecolor(c)
751761
self.set_edgecolor(c)
752762

763+
def _get_default_facecolor(self):
764+
# This may be overridden in a subclass.
765+
return mpl.rcParams['patch.facecolor']
766+
753767
def _set_facecolor(self, c):
754768
if c is None:
755-
c = mpl.rcParams['patch.facecolor']
769+
c = self._get_default_facecolor()
756770

757-
self._is_filled = True
758-
try:
759-
if c.lower() == 'none':
760-
self._is_filled = False
761-
except AttributeError:
762-
pass
763771
self._facecolors = mcolors.to_rgba_array(c, self._alpha)
764772
self.stale = True
765773

@@ -775,6 +783,8 @@ def set_facecolor(self, c):
775783
----------
776784
c : color or list of colors
777785
"""
786+
if isinstance(c, str) and c.lower() in ("none", "face"):
787+
c = c.lower()
778788
self._original_facecolor = c
779789
self._set_facecolor(c)
780790

@@ -787,29 +797,24 @@ def get_edgecolor(self):
787797
else:
788798
return self._edgecolors
789799

800+
def _get_default_edgecolor(self):
801+
# This may be overridden in a subclass.
802+
return mpl.rcParams['patch.edgecolor']
803+
790804
def _set_edgecolor(self, c):
791805
set_hatch_color = True
792806
if c is None:
793-
if (mpl.rcParams['patch.force_edgecolor'] or
794-
not self._is_filled or self._edge_default):
795-
c = mpl.rcParams['patch.edgecolor']
807+
if (mpl.rcParams['patch.force_edgecolor']
808+
or self._edge_default
809+
or cbook._str_equal(self._original_facecolor, 'none')):
810+
c = self._get_default_edgecolor()
796811
else:
797812
c = 'none'
798813
set_hatch_color = False
799-
800-
self._is_stroked = True
801-
try:
802-
if c.lower() == 'none':
803-
self._is_stroked = False
804-
except AttributeError:
805-
pass
806-
807-
try:
808-
if c.lower() == 'face': # Special case: lookup in "get" method.
809-
self._edgecolors = 'face'
810-
return
811-
except AttributeError:
812-
pass
814+
if cbook._str_lower_equal(c, 'face'):
815+
self._edgecolors = 'face'
816+
self.stale = True
817+
return
813818
self._edgecolors = mcolors.to_rgba_array(c, self._alpha)
814819
if set_hatch_color and len(self._edgecolors):
815820
self._hatch_color = tuple(self._edgecolors[0])
@@ -825,6 +830,11 @@ def set_edgecolor(self, c):
825830
The collection edgecolor(s). If a sequence, the patches cycle
826831
through it. If 'face', match the facecolor.
827832
"""
833+
# We pass through a default value for use in LineCollection.
834+
# This allows us to maintain None as the default indicator in
835+
# _original_edgecolor.
836+
if isinstance(c, str) and c.lower() in ("none", "face"):
837+
c = c.lower()
828838
self._original_edgecolor = c
829839
self._set_edgecolor(c)
830840

@@ -853,36 +863,81 @@ def get_linewidth(self):
853863
def get_linestyle(self):
854864
return self._linestyles
855865

866+
def _set_mappable_flags(self):
867+
"""
868+
Determine whether edges and/or faces are color-mapped.
869+
870+
This is a helper for update_scalarmappable.
871+
It sets Boolean flags '_edge_is_mapped' and '_face_is_mapped'.
872+
873+
Returns
874+
-------
875+
mapping_change : bool
876+
True if either flag is True, or if a flag has changed.
877+
"""
878+
# The flags are initialized to None to ensure this returns True
879+
# the first time it is called.
880+
edge0 = self._edge_is_mapped
881+
face0 = self._face_is_mapped
882+
# After returning, the flags must be Booleans, not None.
883+
self._edge_is_mapped = False
884+
self._face_is_mapped = False
885+
if self._A is not None:
886+
if not cbook._str_equal(self._original_facecolor, 'none'):
887+
self._face_is_mapped = True
888+
if cbook._str_equal(self._original_edgecolor, 'face'):
889+
self._edge_is_mapped = True
890+
else:
891+
if self._original_edgecolor is None:
892+
self._edge_is_mapped = True
893+
894+
mapped = self._face_is_mapped or self._edge_is_mapped
895+
changed = (edge0 is None or face0 is None
896+
or self._edge_is_mapped != edge0
897+
or self._face_is_mapped != face0)
898+
return mapped or changed
899+
856900
def update_scalarmappable(self):
857-
"""Update colors from the scalar mappable array, if it is not None."""
858-
if self._A is None:
859-
return
860-
# QuadMesh can map 2d arrays (but pcolormesh supplies 1d array)
861-
if self._A.ndim > 1 and not isinstance(self, QuadMesh):
862-
raise ValueError('Collections can only map rank 1 arrays')
863-
if not self._check_update("array"):
901+
"""
902+
Update colors from the scalar mappable array, if any.
903+
904+
Assign colors to edges and faces based on the array and/or
905+
colors that were directly set, as appropriate.
906+
"""
907+
if not self._set_mappable_flags():
864908
return
865-
if np.iterable(self._alpha):
866-
if self._alpha.size != self._A.size:
867-
raise ValueError(f'Data array shape, {self._A.shape} '
868-
'is incompatible with alpha array shape, '
869-
f'{self._alpha.shape}. '
870-
'This can occur with the deprecated '
871-
'behavior of the "flat" shading option, '
872-
'in which a row and/or column of the data '
873-
'array is dropped.')
874-
# pcolormesh, scatter, maybe others flatten their _A
875-
self._alpha = self._alpha.reshape(self._A.shape)
876-
877-
if self._is_filled:
878-
self._facecolors = self.to_rgba(self._A, self._alpha)
879-
elif self._is_stroked:
880-
self._edgecolors = self.to_rgba(self._A, self._alpha)
909+
# Allow possibility to call 'self.set_array(None)'.
910+
if self._check_update("array") and self._A is not None:
911+
# QuadMesh can map 2d arrays (but pcolormesh supplies 1d array)
912+
if self._A.ndim > 1 and not isinstance(self, QuadMesh):
913+
raise ValueError('Collections can only map rank 1 arrays')
914+
if np.iterable(self._alpha):
915+
if self._alpha.size != self._A.size:
916+
raise ValueError(
917+
f'Data array shape, {self._A.shape} '
918+
'is incompatible with alpha array shape, '
919+
f'{self._alpha.shape}. '
920+
'This can occur with the deprecated '
921+
'behavior of the "flat" shading option, '
922+
'in which a row and/or column of the data '
923+
'array is dropped.')
924+
# pcolormesh, scatter, maybe others flatten their _A
925+
self._alpha = self._alpha.reshape(self._A.shape)
926+
self._mapped_colors = self.to_rgba(self._A, self._alpha)
927+
928+
if self._face_is_mapped:
929+
self._facecolors = self._mapped_colors
930+
else:
931+
self._set_facecolor(self._original_facecolor)
932+
if self._edge_is_mapped:
933+
self._edgecolors = self._mapped_colors
934+
else:
935+
self._set_edgecolor(self._original_edgecolor)
881936
self.stale = True
882937

883938
def get_fill(self):
884-
"""Return whether fill is set."""
885-
return self._is_filled
939+
"""Return whether face is colored."""
940+
return not cbook._str_lower_equal(self._original_facecolor, "none")
886941

887942
def update_from(self, other):
888943
"""Copy properties from other to self."""
@@ -1350,18 +1405,9 @@ class LineCollection(Collection):
13501405

13511406
_edge_default = True
13521407

1353-
def __init__(self, segments, # Can be None.
1354-
linewidths=None,
1355-
colors=None,
1356-
antialiaseds=None,
1357-
linestyles='solid',
1358-
offsets=None,
1359-
transOffset=None,
1360-
norm=None,
1361-
cmap=None,
1362-
pickradius=5,
1363-
zorder=2,
1364-
facecolors='none',
1408+
def __init__(self, segments, # Can be None.
1409+
*args, # Deprecated.
1410+
zorder=2, # Collection.zorder is 1
13651411
**kwargs
13661412
):
13671413
"""
@@ -1394,29 +1440,25 @@ def __init__(self, segments, # Can be None.
13941440
`~.path.Path.CLOSEPOLY`.
13951441
13961442
**kwargs
1397-
Forwareded to `.Collection`.
1443+
Forwarded to `.Collection`.
13981444
"""
1399-
if colors is None:
1400-
colors = mpl.rcParams['lines.color']
1401-
if linewidths is None:
1402-
linewidths = (mpl.rcParams['lines.linewidth'],)
1403-
if antialiaseds is None:
1404-
antialiaseds = (mpl.rcParams['lines.antialiased'],)
1405-
1406-
colors = mcolors.to_rgba_array(colors)
1445+
argnames = ["linewidths", "colors", "antialiaseds", "linestyles",
1446+
"offsets", "transOffset", "norm", "cmap", "pickradius",
1447+
"zorder", "facecolors"]
1448+
if args:
1449+
argkw = {name: val for name, val in zip(argnames, args)}
1450+
kwargs.update(argkw)
1451+
cbook.warn_deprecated(
1452+
"3.4", message="Since %(since)s, passing LineCollection "
1453+
"arguments other than the first, 'segments', as positional "
1454+
"arguments is deprecated, and they will become keyword-only "
1455+
"arguments %(removal)s."
1456+
)
1457+
# Unfortunately, mplot3d needs this explicit setting of 'facecolors'.
1458+
kwargs.setdefault('facecolors', 'none')
14071459
super().__init__(
1408-
edgecolors=colors,
1409-
facecolors=facecolors,
1410-
linewidths=linewidths,
1411-
linestyles=linestyles,
1412-
antialiaseds=antialiaseds,
1413-
offsets=offsets,
1414-
transOffset=transOffset,
1415-
norm=norm,
1416-
cmap=cmap,
14171460
zorder=zorder,
14181461
**kwargs)
1419-
14201462
self.set_segments(segments)
14211463

14221464
def set_segments(self, segments):
@@ -1468,19 +1510,32 @@ def _add_offsets(self, segs):
14681510
segs[i] = segs[i] + offsets[io:io + 1]
14691511
return segs
14701512

1513+
def _get_default_linewidth(self):
1514+
return mpl.rcParams['lines.linewidth']
1515+
1516+
def _get_default_antialiased(self):
1517+
return mpl.rcParams['lines.antialiased']
1518+
1519+
def _get_default_edgecolor(self):
1520+
return mpl.rcParams['lines.color']
1521+
1522+
def _get_default_facecolor(self):
1523+
return 'none'
1524+
14711525
def set_color(self, c):
14721526
"""
1473-
Set the color(s) of the LineCollection.
1527+
Set the edgecolor(s) of the LineCollection.
14741528
14751529
Parameters
14761530
----------
14771531
c : color or list of colors
1478-
Single color (all patches have same color), or a
1479-
sequence of rgba tuples; if it is a sequence the patches will
1532+
Single color (all lines have same color), or a
1533+
sequence of rgba tuples; if it is a sequence the lines will
14801534
cycle through the sequence.
14811535
"""
14821536
self.set_edgecolor(c)
1483-
self.stale = True
1537+
1538+
set_colors = set_color
14841539

14851540
def get_color(self):
14861541
return self._edgecolors
@@ -1851,7 +1906,6 @@ def __init__(self, triangulation, **kwargs):
18511906
super().__init__(**kwargs)
18521907
self._triangulation = triangulation
18531908
self._shading = 'gouraud'
1854-
self._is_filled = True
18551909

18561910
self._bbox = transforms.Bbox.unit()
18571911

0 commit comments

Comments
 (0)