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 4e15176e060a..65598520fc02 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,10 @@ 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) @@ -839,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 96c1a888e04f..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,6 +73,75 @@ def _scale_dashes(offset, dashes, lw): return scaled_offset, scaled_dashes +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. + """ + + try: + pts = reference*lut[val] + except KeyError: + 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): + + a, b = labels + if comparative is None: + ca, cb = a+"er", b+"er" + 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) + + 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): + """ + 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') + """ + reference = rcParams['lines.linewidth'] + return _convert2points(w, reference, 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') + """ + reference = rcParams['lines.markersize'] + return _convert2points(s, reference, markersize_scaling) + + def segment_hits(cx, cy, x, y, radius): """ Determine if any line segments are within radius of a @@ -1008,11 +1077,17 @@ 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) + if w is None: + w = rcParams['lines.linewidth'] + + w = linewidth2points(w) if self._linewidth != w: self.stale = True @@ -1156,12 +1231,18 @@ 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 lines.linewidth 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) + if self._markeredgewidth != ew: self.stale = True self._markeredgewidth = ew @@ -1192,11 +1273,19 @@ 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) + if sz is None: + sz = rcParams['lines.markersize'] + + 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..2f82a5605f56 100644 --- a/lib/matplotlib/patches.py +++ b/lib/matplotlib/patches.py @@ -351,16 +351,21 @@ 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 lines.linewidth 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) + self._linewidth = mlines.linewidth2points(w) + # 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..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, @@ -959,7 +995,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 +1012,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 +1041,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 +1143,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 +1252,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 +1272,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 +1291,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], diff --git a/lib/matplotlib/tests/test_lines.py b/lib/matplotlib/tests/test_lines.py index e0b6258e9ba5..da4e4c689fa6 100644 --- a/lib/matplotlib/tests/test_lines.py +++ b/lib/matplotlib/tests/test_lines.py @@ -184,3 +184,79 @@ 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]) + + 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. + + with pytest.raises(ValueError): + matplotlib.rcParams['lines.linewidth'] = '2x' + + with pytest.raises(ValueError): + 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): + matplotlib.rcParams['xtick.minor.width'] = 'big' + + with pytest.raises(ValueError): + matplotlib.rcParams['ytick.major.width'] = 'microscopic' + pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy