From 7bf436c426cfc4f7e2766ff0622ebbdd52d7f6ff Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Mon, 22 Aug 2016 16:12:12 -0400 Subject: [PATCH] Merge pull request #6904 from efiring/scatter_edgecolor MNT: Use edgecolor rather than linewidth to control edge display. --- lib/matplotlib/collections.py | 103 +++++++++--------- .../mpl-data/stylelib/classic.mplstyle | 1 + lib/matplotlib/patches.py | 78 +++++++------ lib/matplotlib/rcsetup.py | 13 ++- lib/matplotlib/tests/test_artist.py | 29 +++-- matplotlibrc.template | 7 +- 6 files changed, 118 insertions(+), 113 deletions(-) diff --git a/lib/matplotlib/collections.py b/lib/matplotlib/collections.py index 1050d3e721ae..5f602151f1a6 100644 --- a/lib/matplotlib/collections.py +++ b/lib/matplotlib/collections.py @@ -130,9 +130,10 @@ def __init__(self, # list of unbroadcast/scaled linewidths self._us_lw = [0] self._linewidths = [0] + self._is_filled = True # May be modified by set_facecolor(). - self.set_edgecolor(edgecolors) self.set_facecolor(facecolors) + self.set_edgecolor(edgecolors) self.set_linewidth(linewidths) self.set_linestyle(linestyles) self.set_antialiased(antialiaseds) @@ -494,14 +495,9 @@ def set_linewidth(self, lw): ACCEPTS: float or sequence of floats """ if lw is None: - if (self._edge_default or - mpl.rcParams['_internal.classic_mode'] or - not self._is_filled): - lw = mpl.rcParams['patch.linewidth'] - if lw is None: - lw = mpl.rcParams['lines.linewidth'] - else: - lw = 0 + lw = mpl.rcParams['patch.linewidth'] + if lw is None: + lw = mpl.rcParams['lines.linewidth'] # get the un-scaled/broadcast lw self._us_lw = self._get_value(lw) @@ -646,6 +642,20 @@ def set_color(self, c): self.set_facecolor(c) self.set_edgecolor(c) + def _set_facecolor(self, c): + if c is None: + c = mpl.rcParams['patch.facecolor'] + + self._is_filled = True + try: + if c.lower() == 'none': + self._is_filled = False + except AttributeError: + pass + self._facecolors = mcolors.to_rgba_array(c, self._alpha) + self.stale = True + + def set_facecolor(self, c): """ Set the facecolor(s) of the collection. *c* can be a @@ -657,17 +667,9 @@ def set_facecolor(self, c): ACCEPTS: matplotlib color spec or sequence of specs """ - self._is_filled = True - try: - if c.lower() == 'none': - self._is_filled = False - except AttributeError: - pass - if c is None: - c = mpl.rcParams['patch.facecolor'] - self._facecolors_original = c - self._facecolors = mcolors.to_rgba_array(c, self._alpha) - self.stale = True + self._original_facecolor = c + self._set_facecolor(c) + def set_facecolors(self, c): """alias for set_facecolor""" @@ -685,38 +687,45 @@ def get_edgecolor(self): return self._edgecolors get_edgecolors = get_edgecolor - def set_edgecolor(self, c): - """ - Set the edgecolor(s) of the collection. *c* can be a - matplotlib color spec (all patches have same color), or a - sequence of specs; if it is a sequence the patches will - cycle through the sequence. - - If *c* is 'face', the edge color will always be the same as - the face color. If it is 'none', the patch boundary will not - be drawn. - - ACCEPTS: matplotlib color spec or sequence of specs - """ + def _set_edgecolor(self, c): + if c is None: + if (mpl.rcParams['patch.force_edgecolor'] or + not self._is_filled or self._edge_default): + c = mpl.rcParams['patch.edgecolor'] + else: + c = 'none' self._is_stroked = True try: if c.lower() == 'none': self._is_stroked = False except AttributeError: pass + try: - if c.lower() == 'face': + if c.lower() == 'face': # Special case: lookup in "get" method. self._edgecolors = 'face' - self._edgecolors_original = 'face' return except AttributeError: pass - if c is None: - c = mpl.rcParams['patch.edgecolor'] - self._edgecolors_original = c self._edgecolors = mcolors.to_rgba_array(c, self._alpha) self.stale = True + def set_edgecolor(self, c): + """ + Set the edgecolor(s) of the collection. *c* can be a + matplotlib color spec (all patches have same color), or a + sequence of specs; if it is a sequence the patches will + cycle through the sequence. + + If *c* is 'face', the edge color will always be the same as + the face color. If it is 'none', the patch boundary will not + be drawn. + + ACCEPTS: matplotlib color spec or sequence of specs + """ + self._original_edgecolor = c + self._set_edgecolor(c) + def set_edgecolors(self, c): """alias for set_edgecolor""" return self.set_edgecolor(c) @@ -734,18 +743,8 @@ def set_alpha(self, alpha): except TypeError: raise TypeError('alpha must be a float or None') artist.Artist.set_alpha(self, alpha) - try: - self._facecolors = mcolors.to_rgba_array( - self._facecolors_original, self._alpha) - except (AttributeError, TypeError, IndexError): - pass - try: - if (not isinstance(self._edgecolors_original, six.string_types) - or self._edgecolors_original != str('face')): - self._edgecolors = mcolors.to_rgba_array( - self._edgecolors_original, self._alpha) - except (AttributeError, TypeError, IndexError): - pass + self._set_facecolor(self._original_facecolor) + self._set_edgecolor(self._original_edgecolor) def get_linewidths(self): return self._linewidths @@ -781,9 +780,9 @@ def update_from(self, other): artist.Artist.update_from(self, other) self._antialiaseds = other._antialiaseds - self._edgecolors_original = other._edgecolors_original + self._original_edgecolor = other._original_edgecolor self._edgecolors = other._edgecolors - self._facecolors_original = other._facecolors_original + self._original_facecolor = other._original_facecolor self._facecolors = other._facecolors self._linewidths = other._linewidths self._linestyles = other._linestyles diff --git a/lib/matplotlib/mpl-data/stylelib/classic.mplstyle b/lib/matplotlib/mpl-data/stylelib/classic.mplstyle index 5550eab902d1..23fffeb813dd 100644 --- a/lib/matplotlib/mpl-data/stylelib/classic.mplstyle +++ b/lib/matplotlib/mpl-data/stylelib/classic.mplstyle @@ -29,6 +29,7 @@ markers.fillstyle: full # information on patch properties patch.linewidth : 1.0 # edge width in points patch.facecolor : b +patch.force_edgecolor : True patch.edgecolor : k patch.antialiased : True # render patches in antialiased (no jaggies) diff --git a/lib/matplotlib/patches.py b/lib/matplotlib/patches.py index 7a8ff77c7ba7..022821f8bd8f 100644 --- a/lib/matplotlib/patches.py +++ b/lib/matplotlib/patches.py @@ -154,6 +154,18 @@ def get_verts(self): return polygons[0] return [] + def _process_radius(self, radius): + if radius is not None: + return radius + if cbook.is_numlike(self._picker): + _radius = self._picker + else: + if self.get_edgecolor()[3] == 0: + _radius = 0 + else: + _radius = self.get_linewidth() + return _radius + def contains(self, mouseevent, radius=None): """Test whether the mouse event occurred in the patch. @@ -161,11 +173,7 @@ def contains(self, mouseevent, radius=None): """ if six.callable(self._contains): return self._contains(self, mouseevent) - if radius is None: - if cbook.is_numlike(self._picker): - radius = self._picker - else: - radius = self.get_linewidth() + radius = self._process_radius(radius) inside = self.get_path().contains_point( (mouseevent.x, mouseevent.y), self.get_transform(), radius) return inside, {} @@ -175,11 +183,7 @@ def contains_point(self, point, radius=None): Returns *True* if the given point is inside the path (transformed with its transform attribute). """ - if radius is None: - if cbook.is_numlike(self._picker): - radius = self._picker - else: - radius = self.get_linewidth() + radius = self._process_radius(radius) return self.get_path().contains_point(point, self.get_transform(), radius) @@ -281,37 +285,44 @@ def set_aa(self, aa): """alias for set_antialiased""" return self.set_antialiased(aa) + def _set_edgecolor(self, color): + if color is None: + if (mpl.rcParams['patch.force_edgecolor'] or + not self._fill or self._edge_default): + color = mpl.rcParams['patch.edgecolor'] + else: + color = 'none' + self._edgecolor = colors.to_rgba(color, self._alpha) + self.stale = True + def set_edgecolor(self, color): """ Set the patch edge color - ACCEPTS: mpl color spec, or None for default, or 'none' for no color + ACCEPTS: mpl color spec, None, 'none', or 'auto' """ - if color is None: - color = mpl.rcParams['patch.edgecolor'] self._original_edgecolor = color - self._edgecolor = colors.to_rgba(color, self._alpha) - self.stale = True + self._set_edgecolor(color) def set_ec(self, color): """alias for set_edgecolor""" return self.set_edgecolor(color) + def _set_facecolor(self, color): + if color is None: + color = mpl.rcParams['patch.facecolor'] + alpha = self._alpha if self._fill else 0 + self._facecolor = colors.to_rgba(color, alpha) + self.stale = True + def set_facecolor(self, color): """ Set the patch face color ACCEPTS: mpl color spec, or None for default, or 'none' for no color """ - if color is None: - color = mpl.rcParams['patch.facecolor'] - # save: otherwise changing _fill may lose alpha information self._original_facecolor = color - self._facecolor = colors.to_rgba(color, self._alpha) - if not self._fill: - self._facecolor = list(self._facecolor) - self._facecolor[3] = 0 - self.stale = True + self._set_facecolor(color) def set_fc(self, color): """alias for set_facecolor""" @@ -343,10 +354,9 @@ def set_alpha(self, alpha): except TypeError: raise TypeError('alpha must be a float or None') artist.Artist.set_alpha(self, alpha) - # using self._fill and self._alpha - self.set_facecolor(self._original_facecolor) - self.set_edgecolor(self._original_edgecolor) - self.stale = True + self._set_facecolor(self._facecolor) + self._set_edgecolor(self._original_edgecolor) + # stale is already True def set_linewidth(self, w): """ @@ -355,14 +365,9 @@ def set_linewidth(self, w): ACCEPTS: float or None for default """ if w is None: - if (not self._fill or - self._edge_default or - mpl.rcParams['_internal.classic_mode']): - w = mpl.rcParams['patch.linewidth'] - if w is None: - w = mpl.rcParams['axes.linewidth'] - else: - w = 0 + w = mpl.rcParams['patch.linewidth'] + if w is None: + w = mpl.rcParams['axes.linewidth'] self._linewidth = float(w) # scale the dash pattern by the linewidth @@ -428,7 +433,8 @@ def set_fill(self, b): ACCEPTS: [True | False] """ self._fill = bool(b) - self.set_facecolor(self._original_facecolor) + self._set_facecolor(self._original_facecolor) + self._set_edgecolor(self._original_edgecolor) self.stale = True def get_fill(self): diff --git a/lib/matplotlib/rcsetup.py b/lib/matplotlib/rcsetup.py index 5da0b7c83e9c..acd7dc3f992f 100644 --- a/lib/matplotlib/rcsetup.py +++ b/lib/matplotlib/rcsetup.py @@ -364,7 +364,7 @@ def validate_color(s): 'return a valid color arg' try: if s.lower() == 'none': - return 'None' + return 'none' except AttributeError: pass @@ -891,7 +891,7 @@ def validate_hist_bins(s): 'lines.linewidth': [1.5, validate_float], # line width in points 'lines.linestyle': ['-', six.text_type], # solid line 'lines.color': ['C0', validate_color], # first color in color cycle - 'lines.marker': ['None', six.text_type], # black + 'lines.marker': ['None', six.text_type], # marker name 'lines.markeredgewidth': [1.0, validate_float], 'lines.markersize': [6, validate_float], # markersize, in points 'lines.antialiased': [True, validate_bool], # antialiased (no jaggies) @@ -907,10 +907,11 @@ def validate_hist_bins(s): 'markers.fillstyle': ['full', validate_fillstyle], ## patch props - 'patch.linewidth': [None, validate_float_or_None], # line width in points - 'patch.edgecolor': ['k', validate_color], # black - 'patch.facecolor': ['C0', validate_color], # first color in color cycle - 'patch.antialiased': [True, validate_bool], # antialiased (no jaggies) + 'patch.linewidth': [1.0, validate_float], # line width in points + 'patch.edgecolor': ['k', validate_color], + 'patch.force_edgecolor' : [False, validate_bool], + 'patch.facecolor': ['C0', validate_color], # first color in cycle + 'patch.antialiased': [True, validate_bool], # antialiased (no jaggies) ## hatch props 'hatch.linewidth': [1.0, validate_float], diff --git a/lib/matplotlib/tests/test_artist.py b/lib/matplotlib/tests/test_artist.py index 59a9561d4756..ab50c92fd2f0 100644 --- a/lib/matplotlib/tests/test_artist.py +++ b/lib/matplotlib/tests/test_artist.py @@ -181,21 +181,20 @@ def test_remove(): @image_comparison(baseline_images=["default_edges"], remove_text=True, extensions=['png'], style='default') def test_default_edges(): - with mpl.rc_context({'patch.linewidth': None}): - fig, [[ax1, ax2], [ax3, ax4]] = plt.subplots(2, 2) - - ax1.plot(np.arange(10), np.arange(10), 'x', - np.arange(10) + 1, np.arange(10), 'o') - ax2.bar(np.arange(10), np.arange(10)) - ax3.text(0, 0, "BOX", size=24, bbox=dict(boxstyle='sawtooth')) - ax3.set_xlim((-1, 1)) - ax3.set_ylim((-1, 1)) - pp1 = mpatches.PathPatch( - mpath.Path([(0, 0), (1, 0), (1, 1), (0, 0)], - [mpath.Path.MOVETO, mpath.Path.CURVE3, - mpath.Path.CURVE3, mpath.Path.CLOSEPOLY]), - fc="none", transform=ax4.transData) - ax4.add_patch(pp1) + fig, [[ax1, ax2], [ax3, ax4]] = plt.subplots(2, 2) + + ax1.plot(np.arange(10), np.arange(10), 'x', + np.arange(10) + 1, np.arange(10), 'o') + ax2.bar(np.arange(10), np.arange(10)) + ax3.text(0, 0, "BOX", size=24, bbox=dict(boxstyle='sawtooth')) + ax3.set_xlim((-1, 1)) + ax3.set_ylim((-1, 1)) + pp1 = mpatches.PathPatch( + mpath.Path([(0, 0), (1, 0), (1, 1), (0, 0)], + [mpath.Path.MOVETO, mpath.Path.CURVE3, + mpath.Path.CURVE3, mpath.Path.CLOSEPOLY]), + fc="none", transform=ax4.transData) + ax4.add_patch(pp1) @cleanup diff --git a/matplotlibrc.template b/matplotlibrc.template index 3889f75f7c5a..c20c39dc1ba6 100644 --- a/matplotlibrc.template +++ b/matplotlibrc.template @@ -102,11 +102,10 @@ backend : $TEMPLATE_BACKEND # circles. See # http://matplotlib.org/api/artist_api.html#module-matplotlib.patches # information on patch properties -#patch.linewidth : None # edge width in points. - # If None, use axes.linewidth when patch - # is not filled. +#patch.linewidth : 1 # edge width in points. #patch.facecolor : C0 -#patch.edgecolor : black +#patch.edgecolor : black # if forced, or patch is not filled +#patch.force_edgecolor : False # True to always use edgecolor #patch.antialiased : True # render patches in antialiased (no jaggies) ### HATCHES