From f4e25aabd23ca3a1c2ec62b4ed2496b54c54afd2 Mon Sep 17 00:00:00 2001 From: Fabian Kloosterman Date: Sun, 14 Jan 2018 00:04:08 +0100 Subject: [PATCH 1/4] Added support for relative line width, marker size and marker edge width (e.g. 'x-large', 'xx-thin') and corresponding rcParams. Note: in rc, lines.linewidth and lines.markersize are taken as the reference for relative widths/sizes and cannot be relative themselves. Also, relative width for hatch.linewidth is not (yet) supported. --- lib/matplotlib/collections.py | 5 ++- lib/matplotlib/lines.py | 83 ++++++++++++++++++++++++++++++----- lib/matplotlib/patches.py | 16 ++++--- lib/matplotlib/rcsetup.py | 47 +++++++++++++------- 4 files changed, 117 insertions(+), 34 deletions(-) diff --git a/lib/matplotlib/collections.py b/lib/matplotlib/collections.py index 4e15176e060a..cd0a0405a675 100644 --- a/lib/matplotlib/collections.py +++ b/lib/matplotlib/collections.py @@ -494,7 +494,7 @@ def set_linewidth(self, lw): or a sequence; if it is a sequence the patches will cycle through the sequence - ACCEPTS: float or sequence of floats + ACCEPTS: float, string, or sequence of floats/strings """ if lw is None: lw = mpl.rcParams['patch.linewidth'] @@ -503,6 +503,9 @@ def set_linewidth(self, lw): # get the un-scaled/broadcast lw self._us_lw = np.atleast_1d(np.asarray(lw)) + # convert line widths to points + self._us_lw = np.array([mlines.linewidth2points(x) for x in self._us_lw]) + # scale all of the dash patterns. self._linewidths, self._linestyles = self._bcast_lwls( self._us_lw, self._us_linestyles) diff --git a/lib/matplotlib/lines.py b/lib/matplotlib/lines.py index 96c1a888e04f..fd2c35e6474c 100644 --- a/lib/matplotlib/lines.py +++ b/lib/matplotlib/lines.py @@ -73,6 +73,64 @@ def _scale_dashes(offset, dashes, lw): return scaled_offset, scaled_dashes +def _convert2points(val, default, lut): + """ + Convert relative size to points, using `default` as the base value and + `lut` as the look-up table for qualitative size names. + """ + if val is None: + return default + + try: + return default*lut[val] + except KeyError: + pts = float(val) + # do not use relative fraction as string, because when loading styles from file + # there is no way to distinguish between a proper float and its string equivalent + #if isinstance(val, six.string_types): + # pts *= default + return pts + +def _build_qualitative_scaling(labels, comparative=None, base=1.2): + + a, b = labels + if comparative is None: + ca, cb = a+"er", b+"er" + else: + ca, cb = comparative + + d = {None: 1., 'medium': 1., ca:base**-1, cb:base} + for k,m in enumerate(('', 'x-', 'xx-')): + d['{}{}'.format(m,a)] = base**(-k-1) + d['{}{}'.format(m,b)] = base**(k+1) + + return d + +linewidth_scaling = _build_qualitative_scaling(('thin', 'thick'), ('thinner', 'thicker'), 1.5) +markersize_scaling = _build_qualitative_scaling(('small', 'large'), ('smaller', 'larger'), 1.5) + +def linewidth2points(w, default=None): + """ + Convert a line width specification to points. + Line width can be either specified as float (absolute width in points), + a string representing a fraction of the default width in rcParams + or a string representing a relative qualitative width (e.g. 'x-thin') + """ + # the default value may be relative as well! (e.g. for markeredgewidth) + default = _convert2points(default, rcParams['lines.linewidth'], linewidth_scaling) + return _convert2points(w, default, linewidth_scaling) + +def markersize2points(s, default=None): + """ + Convert a marker size specification to points. + Marker size can be either specified as float (absolute size in points), + a string representing a fraction of the default size in rcParams + or a string representing a relative qualitative size (e.g. 'x-large') + """ + default = _convert2points(default, rcParams['lines.markersize'], markersize_scaling) + return _convert2points(s, default, markersize_scaling) + + def segment_hits(cx, cy, x, y, radius): """ Determine if any line segments are within radius of a @@ -1008,11 +1066,12 @@ def set_drawstyle(self, drawstyle): def set_linewidth(self, w): """ - Set the line width in points + Set the line width, either absolute width in points or width relative to rc default. - ACCEPTS: float value in points + ACCEPTS: [float value in points | fraction as string | None | 'xx-thin' | 'x-thin' | + 'thin' | 'thinner' | 'medium' | 'thicker' | 'thick' | 'x-thick' | 'xx-thick'] """ - w = float(w) + w = linewidth2points(w) if self._linewidth != w: self.stale = True @@ -1156,12 +1215,14 @@ def set_markeredgecolor(self, ec): def set_markeredgewidth(self, ew): """ - Set the marker edge width in points + Set the marker edge width, either absolute width in points or + width relative to rc default. - ACCEPTS: float value in points + ACCEPTS: [float value in points | fraction as string | None | 'xx-thin' | 'x-thin' | + 'thin' | 'thinner' | 'medium' | 'thicker' | 'thick' | 'x-thick' | 'xx-thick'] """ - if ew is None: - ew = rcParams['lines.markeredgewidth'] + ew = linewidth2points(ew, rcParams['lines.markeredgewidth']) + if self._markeredgewidth != ew: self.stale = True self._markeredgewidth = ew @@ -1192,11 +1253,13 @@ def set_markerfacecoloralt(self, fc): def set_markersize(self, sz): """ - Set the marker size in points + Set the line width, either absolute width in points or width relative to rc default. - ACCEPTS: float + ACCEPTS: [float value in points | fraction as string | None | 'xx-small' | 'x-small' | + 'small' | 'smaller' | 'medium' | 'larger' | 'large' | 'x-large' | 'xx-large'] """ - sz = float(sz) + sz = markersize2points(sz) + if self._markersize != sz: self.stale = True self._markersize = sz diff --git a/lib/matplotlib/patches.py b/lib/matplotlib/patches.py index 6302fdf72078..0d72f7ab6c6e 100644 --- a/lib/matplotlib/patches.py +++ b/lib/matplotlib/patches.py @@ -351,16 +351,18 @@ def set_alpha(self, alpha): def set_linewidth(self, w): """ - Set the patch linewidth in points + Set the path line width, either absolute width in points or width relative to rc default. - ACCEPTS: float or None for default + ACCEPTS: [float value in points | fraction as string | None | 'xx-thin' | 'x-thin' | + 'thin' | 'thinner' | 'medium' | 'thicker' | 'thick' | 'x-thick' | 'xx-thick'] """ - if w is None: - w = mpl.rcParams['patch.linewidth'] - if w is None: - w = mpl.rcParams['axes.linewidth'] - self._linewidth = float(w) + default = mpl.rcParams['patch.linewidth'] + if default is None: + default = mpl.rcParams['axes.linewidth'] + + self._linewidth = mlines.linewidth2points(w, default) + # scale the dash pattern by the linewidth offset, ls = self._us_dashes self._dashoffset, self._dashes = mlines._scale_dashes( diff --git a/lib/matplotlib/rcsetup.py b/lib/matplotlib/rcsetup.py index eafc8d4eecf7..924bb31b3281 100644 --- a/lib/matplotlib/rcsetup.py +++ b/lib/matplotlib/rcsetup.py @@ -931,6 +931,21 @@ def _validate_linestyle(ls): raise ValueError("linestyle {!r} is not a valid on-off ink " "sequence.".format(ls)) +def _validate_linewidth(w): + try: + w=float(w) + except (ValueError, TypeError): + if w not in ['xx-thin','x-thin','thin','thinner','medium','thick','thicker','x-thick','xx-thick',None]: + raise ValueError("width {!r} is not a valid float or string.".format(w)) + return w + +def _validate_markersize(sz): + try: + sz=float(sz) + except (ValueError, TypeError): + if sz not in ['xx-small','x-small','small','smaller','medium','large','larger','x-large','xx-large',None]: + raise ValueError("marker size {!r} is not a valid float or string.".format(w)) + return sz # a map from key -> value, converter defaultParams = { @@ -959,7 +974,7 @@ def _validate_linestyle(ls): 'lines.linestyle': ['-', _validate_linestyle], # solid line 'lines.color': ['C0', validate_color], # first color in color cycle 'lines.marker': ['None', validate_string], # marker name - 'lines.markeredgewidth': [1.0, validate_float], + 'lines.markeredgewidth': [1.0, _validate_linewidth], # width in points, or relative to lines.linewidth 'lines.markersize': [6, validate_float], # markersize, in points 'lines.antialiased': [True, validate_bool], # antialiased (no jaggies) 'lines.dash_joinstyle': ['round', validate_joinstyle], @@ -976,7 +991,7 @@ def _validate_linestyle(ls): 'markers.fillstyle': ['full', validate_fillstyle], ## patch props - 'patch.linewidth': [1.0, validate_float], # line width in points + 'patch.linewidth': [1.0, _validate_linewidth], # line width in points, or relative to lines.linewidth 'patch.edgecolor': ['k', validate_color], 'patch.force_edgecolor' : [False, validate_bool], 'patch.facecolor': ['C0', validate_color], # first color in cycle @@ -1005,33 +1020,33 @@ def _validate_linestyle(ls): 'boxplot.flierprops.marker': ['o', validate_string], 'boxplot.flierprops.markerfacecolor': ['none', validate_color_or_auto], 'boxplot.flierprops.markeredgecolor': ['k', validate_color], - 'boxplot.flierprops.markersize': [6, validate_float], + 'boxplot.flierprops.markersize': [6, _validate_markersize], 'boxplot.flierprops.linestyle': ['none', _validate_linestyle], - 'boxplot.flierprops.linewidth': [1.0, validate_float], + 'boxplot.flierprops.linewidth': [1.0, _validate_linewidth], 'boxplot.boxprops.color': ['k', validate_color], - 'boxplot.boxprops.linewidth': [1.0, validate_float], + 'boxplot.boxprops.linewidth': [1.0, _validate_linewidth], 'boxplot.boxprops.linestyle': ['-', _validate_linestyle], 'boxplot.whiskerprops.color': ['k', validate_color], - 'boxplot.whiskerprops.linewidth': [1.0, validate_float], + 'boxplot.whiskerprops.linewidth': [1.0, _validate_linewidth], 'boxplot.whiskerprops.linestyle': ['-', _validate_linestyle], 'boxplot.capprops.color': ['k', validate_color], - 'boxplot.capprops.linewidth': [1.0, validate_float], + 'boxplot.capprops.linewidth': [1.0, _validate_linewidth], 'boxplot.capprops.linestyle': ['-', _validate_linestyle], 'boxplot.medianprops.color': ['C1', validate_color], - 'boxplot.medianprops.linewidth': [1.0, validate_float], + 'boxplot.medianprops.linewidth': [1.0, _validate_linewidth], 'boxplot.medianprops.linestyle': ['-', _validate_linestyle], 'boxplot.meanprops.color': ['C2', validate_color], 'boxplot.meanprops.marker': ['^', validate_string], 'boxplot.meanprops.markerfacecolor': ['C2', validate_color], 'boxplot.meanprops.markeredgecolor': ['C2', validate_color], - 'boxplot.meanprops.markersize': [6, validate_float], + 'boxplot.meanprops.markersize': [6, _validate_markersize], 'boxplot.meanprops.linestyle': ['--', _validate_linestyle], - 'boxplot.meanprops.linewidth': [1.0, validate_float], + 'boxplot.meanprops.linewidth': [1.0, _validate_linewidth], ## font props 'font.family': [['sans-serif'], validate_stringlist], # used by text object @@ -1107,7 +1122,7 @@ def _validate_linestyle(ls): 'axes.hold': [None, deprecate_axes_hold], 'axes.facecolor': ['w', validate_color], # background color; white 'axes.edgecolor': ['k', validate_color], # edge color; black - 'axes.linewidth': [0.8, validate_float], # edge linewidth + 'axes.linewidth': [0.8, _validate_linewidth], # edge linewidth 'axes.spines.left': [True, validate_bool], # Set visibility of axes 'axes.spines.right': [True, validate_bool], # 'spines', the lines @@ -1216,8 +1231,8 @@ def _validate_linestyle(ls): 'xtick.bottom': [True, validate_bool], # draw ticks on the bottom side 'xtick.major.size': [3.5, validate_float], # major xtick size in points 'xtick.minor.size': [2, validate_float], # minor xtick size in points - 'xtick.major.width': [0.8, validate_float], # major xtick width in points - 'xtick.minor.width': [0.6, validate_float], # minor xtick width in points + 'xtick.major.width': [0.8, _validate_linewidth], # major xtick width in points + 'xtick.minor.width': [0.6, _validate_linewidth], # minor xtick width in points 'xtick.major.pad': [3.5, validate_float], # distance to label in points 'xtick.minor.pad': [3.4, validate_float], # distance to label in points 'xtick.color': ['k', validate_color], # color of the xtick labels @@ -1236,8 +1251,8 @@ def _validate_linestyle(ls): 'ytick.right': [False, validate_bool], # draw ticks on the right side 'ytick.major.size': [3.5, validate_float], # major ytick size in points 'ytick.minor.size': [2, validate_float], # minor ytick size in points - 'ytick.major.width': [0.8, validate_float], # major ytick width in points - 'ytick.minor.width': [0.6, validate_float], # minor ytick width in points + 'ytick.major.width': [0.8, _validate_linewidth], # major ytick width in points + 'ytick.minor.width': [0.6, _validate_linewidth], # minor ytick width in points 'ytick.major.pad': [3.5, validate_float], # distance to label in points 'ytick.minor.pad': [3.4, validate_float], # distance to label in points 'ytick.color': ['k', validate_color], # color of the ytick labels @@ -1255,7 +1270,7 @@ def _validate_linestyle(ls): 'grid.color': ['#b0b0b0', validate_color], # grid color 'grid.linestyle': ['-', _validate_linestyle], # solid - 'grid.linewidth': [0.8, validate_float], # in points + 'grid.linewidth': [0.8, _validate_linewidth], # in points 'grid.alpha': [1.0, validate_float], From 77975e4cf989b902020030d2985f4a15df9f405f Mon Sep 17 00:00:00 2001 From: Fabian Kloosterman Date: Sun, 14 Jan 2018 01:29:57 +0100 Subject: [PATCH 2/4] Added test for setting relative line width, marker edge width and marker size. --- lib/matplotlib/tests/test_lines.py | 46 ++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/lib/matplotlib/tests/test_lines.py b/lib/matplotlib/tests/test_lines.py index e0b6258e9ba5..6483b5dbc3b4 100644 --- a/lib/matplotlib/tests/test_lines.py +++ b/lib/matplotlib/tests/test_lines.py @@ -184,3 +184,49 @@ def test_nan_is_sorted(): assert line._is_sorted(np.array([1, 2, 3])) assert line._is_sorted(np.array([1, np.nan, 3])) assert not line._is_sorted([3, 5] + [np.nan] * 100 + [0, 2]) + +def test_relative_sizes(): + line = mlines.Line2D([0,1],[0,1]) + + base = matplotlib.rcParams['lines.linewidth'] + + # use default line with from rcParams + line.set_linewidth(None) + assert line.get_linewidth() == base + + line.set_linewidth('auto') + assert line.get_linewidth() == base + + # set qualitative relative widths + for w in ('xx-thin', 'x-thin', 'thin', 'thinner', + 'medium', 'thick', 'thicker', 'x-thick', 'xx-thick'): + line.set_linewidth(w) + assert line.get_linewidth() == mlines.linewidth_scaling[w]*base + + line.set_markeredgewidth(w) + assert line.get_markeredgewidth() == mlines.linewidth_scaling[w]*base + + base = matplotlib.rcParams['lines.markersize'] + + # use default marker size + line.set_markersize(None) + assert line.get_markersize() == base + + line.set_markersize('auto') + assert line.get_markersize() == base + + # set qualitative relative size + for sz in ('xx-small', 'x-small', 'small', 'smaller', + 'medium', 'large', 'larger', 'x-large', 'xx-large'): + line.set_markersize(sz) + assert line.get_markersize() == mlines.markersize_scaling[sz]*base + + + with pytest.raises(ValueError): + line.set_linewidth('large') + + with pytest.raises(ValueError): + line.set_markeredgewidth('six') + + with pytest.raises(ValueError): + line.set_markersize('tall') From f229a8180aa531cc6d02a9b6a5bf746c80b31b40 Mon Sep 17 00:00:00 2001 From: Fabian Kloosterman Date: Sun, 14 Jan 2018 01:30:44 +0100 Subject: [PATCH 3/4] Separate default and reference value when performing size conversion. --- lib/matplotlib/lines.py | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/lib/matplotlib/lines.py b/lib/matplotlib/lines.py index fd2c35e6474c..7b711babd96f 100644 --- a/lib/matplotlib/lines.py +++ b/lib/matplotlib/lines.py @@ -73,16 +73,17 @@ def _scale_dashes(offset, dashes, lw): return scaled_offset, scaled_dashes -def _convert2points(val, default, lut): +def _convert2points(val, default, reference, lut): """ - Convert relative size to points, using `default` as the base value and - `lut` as the look-up table for qualitative size names. + Convert relative size to points, using `reference` as the base value and + `lut` as the look-up table for qualitative size names. If `val` is None or 'auto', + then the `default` is returned """ - if val is None: + if val is None or val=='auto': return default try: - return default*lut[val] + return reference*lut[val] except KeyError: pts = float(val) # do not use relative fraction as string, because when loading styles from file @@ -99,7 +100,7 @@ def _build_qualitative_scaling(labels, comparative=None, base=1.2): else: ca, cb = comparative - d = {None: 1., 'medium': 1., ca:base**-1, cb:base} + d = {'medium': 1., ca:base**-1, cb:base} for k,m in enumerate(('', 'x-', 'xx-')): d['{}{}'.format(m,a)] = base**(-k-1) d['{}{}'.format(m,b)] = base**(k+1) @@ -117,8 +118,9 @@ def linewidth2points(w, default=None): or a string representing a relative qualitative width (e.g. 'x-thin') """ # the default value may be relative as well! (e.g. for markeredgewidth) - default = _convert2points(default, rcParams['lines.linewidth'], linewidth_scaling) - return _convert2points(w, default, linewidth_scaling) + default = _convert2points(default, rcParams['lines.linewidth'], + rcParams['lines.linewidth'], linewidth_scaling) + return _convert2points(w, default, rcParams['lines.linewidth'], linewidth_scaling) def markersize2points(s, default=None): """ @@ -127,8 +129,9 @@ def markersize2points(s, default=None): a string representing a fraction of the default size in rcParams or a string representing a relative qualitative size (e.g. 'x-large') """ - default = _convert2points(default, rcParams['lines.markersize'], markersize_scaling) - return _convert2points(s, default, markersize_scaling) + default = _convert2points(default, rcParams['lines.markersize'], + rcParams['lines.markersize'], markersize_scaling) + return _convert2points(s, default, rcParams['lines.markersize'], markersize_scaling) def segment_hits(cx, cy, x, y, radius): From f80c21e08bbba7b7e6907a6e557c210458acedf0 Mon Sep 17 00:00:00 2001 From: Fabian Kloosterman Date: Sun, 14 Jan 2018 16:50:50 +0100 Subject: [PATCH 4/4] Added support for '0.5x' and '120%' style relative widths/sizes Improved validators in rcsetup.py rcsetup.cycler now uses new line width/marker size validators Updated tests --- lib/matplotlib/cbook/__init__.py | 21 ++++++ lib/matplotlib/collections.py | 4 +- lib/matplotlib/lines.py | 97 +++++++++++++++++---------- lib/matplotlib/patches.py | 17 +++-- lib/matplotlib/rcsetup.py | 57 +++++++++++----- lib/matplotlib/tests/test_lines.py | 104 +++++++++++++++++++---------- 6 files changed, 200 insertions(+), 100 deletions(-) diff --git a/lib/matplotlib/cbook/__init__.py b/lib/matplotlib/cbook/__init__.py index 61e66f317fe8..2d6015e42c2b 100644 --- a/lib/matplotlib/cbook/__init__.py +++ b/lib/matplotlib/cbook/__init__.py @@ -2272,6 +2272,27 @@ def pts_to_midstep(x, *args): 'steps-mid': pts_to_midstep} +MEASUREMENT = re.compile( + r'''( # group match like scanf() token %e, %E, %f, %g + [-+]? # +/- or nothing for positive + (\d+(\.\d*)?|\.\d+) # match numbers: 1, 1., 1.1, .1 + ([eE][-+]?\d+)? # scientific notation: e(+/-)2 (*10^2) + ) + (\s*) # separator: white space or nothing + ( # unit of measure: like GB. also works for no units + \S*)''', re.VERBOSE) + + +def parse_measurement(value_sep_units): + measurement = re.match(MEASUREMENT, value_sep_units) + if measurement is None: + raise ValueError("Not a valid measurement value:" + " {!r}".format(value_sep_units)) + value = float(measurement.groups()[0]) + units = measurement.groups()[5] + return value, units + + def index_of(y): """ A helper function to get the index of an input to plot diff --git a/lib/matplotlib/collections.py b/lib/matplotlib/collections.py index cd0a0405a675..65598520fc02 100644 --- a/lib/matplotlib/collections.py +++ b/lib/matplotlib/collections.py @@ -504,7 +504,8 @@ def set_linewidth(self, lw): self._us_lw = np.atleast_1d(np.asarray(lw)) # convert line widths to points - self._us_lw = np.array([mlines.linewidth2points(x) for x in self._us_lw]) + self._us_lw = np.array([mlines.linewidth2points(x) + for x in self._us_lw]) # scale all of the dash patterns. self._linewidths, self._linestyles = self._bcast_lwls( @@ -842,6 +843,7 @@ def update_from(self, other): # self.update_dict = other.update_dict # do we need to copy this? -JJL self.stale = True + # these are not available for the object inspector until after the # class is built so we define an initial set here for the init # function and they will be overridden after object defn diff --git a/lib/matplotlib/lines.py b/lib/matplotlib/lines.py index 7b711babd96f..5f9cdf38b899 100644 --- a/lib/matplotlib/lines.py +++ b/lib/matplotlib/lines.py @@ -17,7 +17,7 @@ from .artist import Artist, allow_rasterization from .cbook import ( _to_unmasked_float_array, iterable, is_numlike, ls_mapper, ls_mapper_r, - STEP_LOOKUP_MAP) + STEP_LOOKUP_MAP, parse_measurement) from .markers import MarkerStyle from .path import Path from .transforms import Bbox, TransformedPath, IdentityTransform @@ -73,24 +73,30 @@ def _scale_dashes(offset, dashes, lw): return scaled_offset, scaled_dashes -def _convert2points(val, default, reference, lut): +def _convert2points(val, reference, lut): """ Convert relative size to points, using `reference` as the base value and - `lut` as the look-up table for qualitative size names. If `val` is None or 'auto', - then the `default` is returned + `lut` as the look-up table for qualitative size names. If `val` is None or + 'auto', then the `default` is returned. """ - if val is None or val=='auto': - return default try: - return reference*lut[val] + pts = reference*lut[val] except KeyError: - pts = float(val) - # do not use relative fraction as string, because when loading styles from file - # there is no way to distinguish between a proper float and its string equivalent - #if isinstance(val, six.string_types): - # pts *= default - return pts + try: + pts = float(val) + except (ValueError, TypeError): + pts, u = parse_measurement(val) + if u not in ['x', '%']: + raise ValueError('Unrecognized relative size value ' + '{!r}'.format(val)) + if u == '%': + pts /= 100. + + pts *= reference + + return pts + def _build_qualitative_scaling(labels, comparative=None, base=1.2): @@ -100,27 +106,30 @@ def _build_qualitative_scaling(labels, comparative=None, base=1.2): else: ca, cb = comparative - d = {'medium': 1., ca:base**-1, cb:base} - for k,m in enumerate(('', 'x-', 'xx-')): - d['{}{}'.format(m,a)] = base**(-k-1) - d['{}{}'.format(m,b)] = base**(k+1) + d = {'medium': 1., ca: base**-1, cb: base} + for k, m in enumerate(('', 'x-', 'xx-')): + d['{}{}'.format(m, a)] = base**(-k-1) + d['{}{}'.format(m, b)] = base**(k+1) return d -linewidth_scaling = _build_qualitative_scaling(('thin', 'thick'), ('thinner', 'thicker'), 1.5) -markersize_scaling = _build_qualitative_scaling(('small', 'large'), ('smaller', 'larger'), 1.5) -def linewidth2points(w, default=None): +linewidth_scaling = _build_qualitative_scaling( + ('thin', 'thick'), ('thinner', 'thicker'), 1.5) +markersize_scaling = _build_qualitative_scaling( + ('small', 'large'), ('smaller', 'larger'), 1.5) + + +def linewidth2points(w): """ Convert a line width specification to points. Line width can be either specified as float (absolute width in points), a string representing a fraction of the default width in rcParams or a string representing a relative qualitative width (e.g. 'x-thin') """ - # the default value may be relative as well! (e.g. for markeredgewidth) - default = _convert2points(default, rcParams['lines.linewidth'], - rcParams['lines.linewidth'], linewidth_scaling) - return _convert2points(w, default, rcParams['lines.linewidth'], linewidth_scaling) + reference = rcParams['lines.linewidth'] + return _convert2points(w, reference, linewidth_scaling) + def markersize2points(s, default=None): """ @@ -129,9 +138,8 @@ def markersize2points(s, default=None): a string representing a fraction of the default size in rcParams or a string representing a relative qualitative size (e.g. 'x-large') """ - default = _convert2points(default, rcParams['lines.markersize'], - rcParams['lines.markersize'], markersize_scaling) - return _convert2points(s, default, rcParams['lines.markersize'], markersize_scaling) + reference = rcParams['lines.markersize'] + return _convert2points(s, reference, markersize_scaling) def segment_hits(cx, cy, x, y, radius): @@ -1069,11 +1077,16 @@ def set_drawstyle(self, drawstyle): def set_linewidth(self, w): """ - Set the line width, either absolute width in points or width relative to rc default. + Set the line width, either absolute width in points or + width relative to rc default. - ACCEPTS: [float value in points | fraction as string | None | 'xx-thin' | 'x-thin' | - 'thin' | 'thinner' | 'medium' | 'thicker' | 'thick' | 'x-thick' | 'xx-thick'] + ACCEPTS: [float value in points | fraction as string | None | + 'xx-thin' | 'x-thin' | 'thin' | 'thinner' | 'medium' | + 'thicker' | 'thick' | 'x-thick' | 'xx-thick'] """ + if w is None: + w = rcParams['lines.linewidth'] + w = linewidth2points(w) if self._linewidth != w: @@ -1219,12 +1232,16 @@ def set_markeredgecolor(self, ec): def set_markeredgewidth(self, ew): """ Set the marker edge width, either absolute width in points or - width relative to rc default. + width relative to lines.linewidth rc default. - ACCEPTS: [float value in points | fraction as string | None | 'xx-thin' | 'x-thin' | - 'thin' | 'thinner' | 'medium' | 'thicker' | 'thick' | 'x-thick' | 'xx-thick'] + ACCEPTS: [float value in points | fraction as string | None | + 'xx-thin' | 'x-thin' | 'thin' | 'thinner' | 'medium' | + 'thicker' | 'thick' | 'x-thick' | 'xx-thick'] """ - ew = linewidth2points(ew, rcParams['lines.markeredgewidth']) + if ew is None: + ew = rcParams['lines.markeredgewidth'] + + ew = linewidth2points(ew) if self._markeredgewidth != ew: self.stale = True @@ -1256,11 +1273,17 @@ def set_markerfacecoloralt(self, fc): def set_markersize(self, sz): """ - Set the line width, either absolute width in points or width relative to rc default. + Set the line width, either absolute width in points or + width relative to rc default. - ACCEPTS: [float value in points | fraction as string | None | 'xx-small' | 'x-small' | - 'small' | 'smaller' | 'medium' | 'larger' | 'large' | 'x-large' | 'xx-large'] + ACCEPTS: [float value in points | fraction as string | None | + 'xx-small' | 'x-small' | 'small' | 'smaller' | + 'medium' | 'larger' | 'large' | 'x-large' | + 'xx-large'] """ + if sz is None: + sz = rcParams['lines.markersize'] + sz = markersize2points(sz) if self._markersize != sz: diff --git a/lib/matplotlib/patches.py b/lib/matplotlib/patches.py index 0d72f7ab6c6e..2f82a5605f56 100644 --- a/lib/matplotlib/patches.py +++ b/lib/matplotlib/patches.py @@ -351,17 +351,20 @@ def set_alpha(self, alpha): def set_linewidth(self, w): """ - Set the path line width, either absolute width in points or width relative to rc default. + Set the path line width, either absolute width in points or + width relative to lines.linewidth rc default. - ACCEPTS: [float value in points | fraction as string | None | 'xx-thin' | 'x-thin' | - 'thin' | 'thinner' | 'medium' | 'thicker' | 'thick' | 'x-thick' | 'xx-thick'] + ACCEPTS: [float value in points | fraction as string | None | + 'xx-thin' | 'x-thin' | 'thin' | 'thinner' | 'medium' | + 'thicker' | 'thick' | 'x-thick' | 'xx-thick'] """ - default = mpl.rcParams['patch.linewidth'] - if default is None: - default = mpl.rcParams['axes.linewidth'] + if w is None: + w = mpl.rcParams['patch.linewidth'] + if w is None: + w = mpl.rcParams['axes.linewidth'] - self._linewidth = mlines.linewidth2points(w, default) + self._linewidth = mlines.linewidth2points(w) # scale the dash pattern by the linewidth offset, ls = self._us_dashes diff --git a/lib/matplotlib/rcsetup.py b/lib/matplotlib/rcsetup.py index 924bb31b3281..23e029f831c0 100644 --- a/lib/matplotlib/rcsetup.py +++ b/lib/matplotlib/rcsetup.py @@ -675,10 +675,46 @@ def validate_hatch(s): validate_hatchlist = _listify_validator(validate_hatch) validate_dashlist = _listify_validator(validate_nseq_float(allow_none=True)) + +def _validate_linewidth(w): + try: + w = float(w) + except (ValueError, TypeError): + if w in ['xx-thin', 'x-thin', 'thin', 'thinner', 'medium', 'thick', + 'thicker', 'x-thick', 'xx-thick']: + return w + else: + val, u = cbook.parse_measurement(w) + if u not in ['x', '%']: + raise ValueError("value {!r} is not a valid absolute" + "or relative width.".format(w)) + + return w + + +def _validate_markersize(sz): + try: + sz = float(sz) + except (ValueError, TypeError): + if sz in ['xx-small', 'x-small', 'small', 'smaller', 'medium', + 'large', 'larger', 'x-large', 'xx-large']: + return sz + else: + val, u = cbook.parse_measurement(sz) + if u not in ['x', '%']: + raise ValueError("value {!r} is not a valid absolute" + "or relative size.".format(sz)) + + return sz + + +validate_linewidthlist = _listify_validator(_validate_linewidth) +validate_markersizelist = _listify_validator(_validate_markersize) + _prop_validators = { 'color': _listify_validator(validate_color_for_prop_cycle, allow_stringlist=True), - 'linewidth': validate_floatlist, + 'linewidth': validate_linewidthlist, 'linestyle': validate_stringlist, 'facecolor': validate_colorlist, 'edgecolor': validate_colorlist, @@ -686,8 +722,8 @@ def validate_hatch(s): 'capstyle': validate_capstylelist, 'fillstyle': validate_fillstylelist, 'markerfacecolor': validate_colorlist, - 'markersize': validate_floatlist, - 'markeredgewidth': validate_floatlist, + 'markersize': validate_markersizelist, + 'markeredgewidth': validate_linewidthlist, 'markeredgecolor': validate_colorlist, 'alpha': validate_floatlist, 'marker': validate_stringlist, @@ -931,21 +967,6 @@ def _validate_linestyle(ls): raise ValueError("linestyle {!r} is not a valid on-off ink " "sequence.".format(ls)) -def _validate_linewidth(w): - try: - w=float(w) - except (ValueError, TypeError): - if w not in ['xx-thin','x-thin','thin','thinner','medium','thick','thicker','x-thick','xx-thick',None]: - raise ValueError("width {!r} is not a valid float or string.".format(w)) - return w - -def _validate_markersize(sz): - try: - sz=float(sz) - except (ValueError, TypeError): - if sz not in ['xx-small','x-small','small','smaller','medium','large','larger','x-large','xx-large',None]: - raise ValueError("marker size {!r} is not a valid float or string.".format(w)) - return sz # a map from key -> value, converter defaultParams = { diff --git a/lib/matplotlib/tests/test_lines.py b/lib/matplotlib/tests/test_lines.py index 6483b5dbc3b4..da4e4c689fa6 100644 --- a/lib/matplotlib/tests/test_lines.py +++ b/lib/matplotlib/tests/test_lines.py @@ -185,48 +185,78 @@ def test_nan_is_sorted(): assert line._is_sorted(np.array([1, np.nan, 3])) assert not line._is_sorted([3, 5] + [np.nan] * 100 + [0, 2]) -def test_relative_sizes(): - line = mlines.Line2D([0,1],[0,1]) - - base = matplotlib.rcParams['lines.linewidth'] - - # use default line with from rcParams - line.set_linewidth(None) - assert line.get_linewidth() == base - - line.set_linewidth('auto') - assert line.get_linewidth() == base - - # set qualitative relative widths - for w in ('xx-thin', 'x-thin', 'thin', 'thinner', - 'medium', 'thick', 'thicker', 'x-thick', 'xx-thick'): - line.set_linewidth(w) - assert line.get_linewidth() == mlines.linewidth_scaling[w]*base - - line.set_markeredgewidth(w) - assert line.get_markeredgewidth() == mlines.linewidth_scaling[w]*base - - base = matplotlib.rcParams['lines.markersize'] - # use default marker size - line.set_markersize(None) - assert line.get_markersize() == base - - line.set_markersize('auto') - assert line.get_markersize() == base +def test_relative_sizes(): + line = mlines.Line2D([0, 1], [0, 1]) + + reference = 1. + with matplotlib.rc_context( + rc={'lines.linewidth': reference, + 'lines.markersize': reference, + 'lines.markeredgewidth': reference}): + + # use default line with from rcParams + line.set(linewidth=None, markeredgewidth=None, markersize=None) + assert line.get_linewidth() == reference + assert line.get_markeredgewidth() == reference + assert line.get_markersize() == reference + + line.set(linewidth='2x', markeredgewidth='0.5x', markersize='2e1x') + assert line.get_linewidth() == 2*reference + assert line.get_markeredgewidth() == 0.5*reference + assert line.get_markersize() == 2e1*reference + + line.set(linewidth='50%', markeredgewidth='2e2 %', markersize='250%') + assert line.get_linewidth() == 0.5*reference + assert line.get_markeredgewidth() == 2*reference + assert line.get_markersize() == 2.5*reference + + # set qualitative relative widths + for w in ('xx-thin', 'x-thin', 'thin', 'thinner', + 'medium', 'thick', 'thicker', 'x-thick', 'xx-thick'): + line.set(linewidth=w, markeredgewidth=w) + assert (line.get_linewidth() == + mlines.linewidth_scaling[w]*reference) + assert (line.get_markeredgewidth() == + mlines.linewidth_scaling[w]*reference) + + # set qualitative relative size + for sz in ('xx-small', 'x-small', 'small', 'smaller', + 'medium', 'large', 'larger', 'x-large', 'xx-large'): + line.set_markersize(sz) + assert (line.get_markersize() == + mlines.markersize_scaling[sz]*reference) + + with pytest.raises(ValueError): + line.set_linewidth('large') + + with pytest.raises(ValueError): + line.set_markeredgewidth('six') + + with pytest.raises(ValueError): + line.set_markersize('tall') + + +def test_relative_sizes_rc(): + + # only absolute width for lines.linewidth + matplotlib.rcParams['lines.linewidth'] = 1. + matplotlib.rcParams['lines.markersize'] = 1. - # set qualitative relative size - for sz in ('xx-small', 'x-small', 'small', 'smaller', - 'medium', 'large', 'larger', 'x-large', 'xx-large'): - line.set_markersize(sz) - assert line.get_markersize() == mlines.markersize_scaling[sz]*base + with pytest.raises(ValueError): + matplotlib.rcParams['lines.linewidth'] = '2x' - with pytest.raises(ValueError): - line.set_linewidth('large') + matplotlib.rcParams['lines.markersize'] = '100%' + + # relative width/size + matplotlib.rcParams['lines.markeredgewidth'] = '0.5x' + matplotlib.rcParams['xtick.major.width'] = '2x' + matplotlib.rcParams['ytick.minor.width'] = '50%' with pytest.raises(ValueError): - line.set_markeredgewidth('six') + matplotlib.rcParams['xtick.minor.width'] = 'big' with pytest.raises(ValueError): - line.set_markersize('tall') + matplotlib.rcParams['ytick.major.width'] = 'microscopic' +