From 8175daaa4fbd8b318a770691137a0ecf73dc1439 Mon Sep 17 00:00:00 2001 From: Bruno Beltran Date: Mon, 5 Oct 2020 11:59:23 -0700 Subject: [PATCH 1/3] GSOD: LineStyle class --- examples/lines_bars_and_markers/linestyles.py | 92 ++---- lib/matplotlib/_enums.py | 265 +++++++++++++++++- lib/matplotlib/cbook/__init__.py | 6 - lib/matplotlib/lines.py | 159 ++--------- lib/matplotlib/patches.py | 36 +-- lib/matplotlib/rcsetup.py | 31 +- 6 files changed, 340 insertions(+), 249 deletions(-) diff --git a/examples/lines_bars_and_markers/linestyles.py b/examples/lines_bars_and_markers/linestyles.py index 35920617c90c..d71488bcfa87 100644 --- a/examples/lines_bars_and_markers/linestyles.py +++ b/examples/lines_bars_and_markers/linestyles.py @@ -3,74 +3,28 @@ Linestyles ========== -Simple linestyles can be defined using the strings "solid", "dotted", "dashed" -or "dashdot". More refined control can be achieved by providing a dash tuple -``(offset, (on_off_seq))``. For example, ``(0, (3, 10, 1, 15))`` means -(3pt line, 10pt space, 1pt line, 15pt space) with no offset. See also -`.Line2D.set_linestyle`. - -*Note*: The dash style can also be configured via `.Line2D.set_dashes` -as shown in :doc:`/gallery/lines_bars_and_markers/line_demo_dash_control` -and passing a list of dash sequences using the keyword *dashes* to the -cycler in :doc:`property_cycle `. +The Matplotlib `~mpl._enums.LineStyle` specifies the dash pattern used to draw +a given line. The simplest line styles can be accessed by name using the +strings "solid", "dotted", "dashed" and "dashdot" (or their short names, "-", +":", "--", and "-.", respectively). + +The exact spacing of the dashes used can be controlled using the +'lines.*_pattern' family of rc parameters. For example, +:rc:`lines.dashdot_pattern` controls the exact spacing of dashed used whenever +the '-.' `~mpl._enums.LineStyle` is specified. + +For more information about how to create custom `~mpl._enums.LineStyle` +specifications, see `the LineStyle docs `. + +*Note*: For historical reasons, one can also specify the dash pattern for a +particular line using `.Line2D.set_dashes` as shown in +:doc:`/gallery/lines_bars_and_markers/line_demo_dash_control` (or by passing a +list of dash sequences using the keyword *dashes* to the cycler in +:doc:`property_cycle `). This interface is +strictly less expressive, and we recommend using LineStyle (or the keyword +*linestyle* to the :doc:`property cycler +`). """ -import numpy as np -import matplotlib.pyplot as plt - -linestyle_str = [ - ('solid', 'solid'), # Same as (0, ()) or '-' - ('dotted', 'dotted'), # Same as (0, (1, 1)) or '.' - ('dashed', 'dashed'), # Same as '--' - ('dashdot', 'dashdot')] # Same as '-.' - -linestyle_tuple = [ - ('loosely dotted', (0, (1, 10))), - ('dotted', (0, (1, 1))), - ('densely dotted', (0, (1, 1))), - - ('loosely dashed', (0, (5, 10))), - ('dashed', (0, (5, 5))), - ('densely dashed', (0, (5, 1))), - - ('loosely dashdotted', (0, (3, 10, 1, 10))), - ('dashdotted', (0, (3, 5, 1, 5))), - ('densely dashdotted', (0, (3, 1, 1, 1))), - - ('dashdotdotted', (0, (3, 5, 1, 5, 1, 5))), - ('loosely dashdotdotted', (0, (3, 10, 1, 10, 1, 10))), - ('densely dashdotdotted', (0, (3, 1, 1, 1, 1, 1)))] - - -def plot_linestyles(ax, linestyles, title): - X, Y = np.linspace(0, 100, 10), np.zeros(10) - yticklabels = [] - - for i, (name, linestyle) in enumerate(linestyles): - ax.plot(X, Y+i, linestyle=linestyle, linewidth=1.5, color='black') - yticklabels.append(name) - - ax.set_title(title) - ax.set(ylim=(-0.5, len(linestyles)-0.5), - yticks=np.arange(len(linestyles)), - yticklabels=yticklabels) - ax.tick_params(left=False, bottom=False, labelbottom=False) - ax.spines[:].set_visible(False) - - # For each line style, add a text annotation with a small offset from - # the reference point (0 in Axes coords, y tick value in Data coords). - for i, (name, linestyle) in enumerate(linestyles): - ax.annotate(repr(linestyle), - xy=(0.0, i), xycoords=ax.get_yaxis_transform(), - xytext=(-6, -12), textcoords='offset points', - color="blue", fontsize=8, ha="right", family="monospace") - - -ax0, ax1 = (plt.figure(figsize=(10, 8)) - .add_gridspec(2, 1, height_ratios=[1, 3]) - .subplots()) - -plot_linestyles(ax0, linestyle_str[::-1], title='Named linestyles') -plot_linestyles(ax1, linestyle_tuple[::-1], title='Parametrized linestyles') +from matplotlib._enums import LineStyle -plt.tight_layout() -plt.show() +LineStyle.demo() diff --git a/lib/matplotlib/_enums.py b/lib/matplotlib/_enums.py index 35fe82482869..594b70f381e3 100644 --- a/lib/matplotlib/_enums.py +++ b/lib/matplotlib/_enums.py @@ -11,7 +11,9 @@ """ from enum import Enum, auto -from matplotlib import cbook, docstring +from numbers import Number + +from matplotlib import _api, docstring class _AutoStringNameEnum(Enum): @@ -28,12 +30,12 @@ def _deprecate_case_insensitive_join_cap(s): s_low = s.lower() if s != s_low: if s_low in ['miter', 'round', 'bevel']: - cbook.warn_deprecated( + _api.warn_deprecated( "3.3", message="Case-insensitive capstyles are deprecated " "since %(since)s and support for them will be removed " "%(removal)s; please pass them in lowercase.") elif s_low in ['butt', 'round', 'projecting']: - cbook.warn_deprecated( + _api.warn_deprecated( "3.3", message="Case-insensitive joinstyles are deprecated " "since %(since)s and support for them will be removed " "%(removal)s; please pass them in lowercase.") @@ -206,3 +208,260 @@ def demo(): docstring.interpd.update({'JoinStyle': JoinStyle.input_description, 'CapStyle': CapStyle.input_description}) + + +#: Maps short codes for line style to their full name used by backends. +_ls_mapper = {'': 'None', ' ': 'None', 'none': 'None', + '-': 'solid', '--': 'dashed', '-.': 'dashdot', ':': 'dotted'} +_deprecated_lineStyles = { + '-': '_draw_solid', + '--': '_draw_dashed', + '-.': '_draw_dash_dot', + ':': '_draw_dotted', + 'None': '_draw_nothing', + ' ': '_draw_nothing', + '': '_draw_nothing', +} + + +class NamedLineStyle(str, _AutoStringNameEnum): + """ + Describe if the line is solid or dashed, and the dash pattern, if any. + + All lines in Matplotlib are considered either solid or "dashed". Some + common dashing patterns are built-in, and are sufficient for a majority of + uses: + + =============================== ================= + Linestyle Description + =============================== ================= + ``'-'`` or ``'solid'`` solid line + ``'--'`` or ``'dashed'`` dashed line + ``'-.'`` or ``'dashdot'`` dash-dotted line + ``':'`` or ``'dotted'`` dotted line + ``'None'`` or ``' '`` or ``''`` draw nothing + =============================== ================= + + However, for more fine-grained control, one can directly specify the + dashing pattern by specifying:: + + (offset, onoffseq) + + where ``onoffseq`` is an even length tuple specifying the lengths of each + subsequent dash and space, and ``offset`` controls at which point in this + pattern the start of the line will begin (to allow you to e.g. prevent + corners from happening to land in between dashes). + + For example, (5, 2, 1, 2) describes a sequence of 5 point and 1 point + dashes separated by 2 point spaces. + + Setting ``onoffseq`` to ``None`` results in a solid *LineStyle*. + + The default dashing patterns described in the table above are themselves + all described in this notation, and can therefore be customized by editing + the appropriate ``lines.*_pattern`` *rc* parameter, as described in + :doc:`/tutorials/introductory/customizing`. + + .. plot:: + :alt: Demo of possible LineStyle's. + + from matplotlib._types import LineStyle + LineStyle.demo() + + .. note:: + + In addition to directly taking a ``linestyle`` argument, + `~.lines.Line2D` exposes a ``~.lines.Line2D.set_dashes`` method that + can be used to create a new *LineStyle* by providing just the + ``onoffseq``, but does not let you customize the offset. This method is + called when using the keyword *dashes* to the cycler , as shown in + :doc:`property_cycle `. + """ + solid = auto() + dashed = auto() + dotted = auto() + dashdot = auto() + none = auto() + custom = auto() + +class LineStyle(str): + + def __init__(self, ls, scale=1): + """ + Parameters + ---------- + ls : str or dash tuple + A description of the dashing pattern of the line. Allowed string + inputs are {'-', 'solid', '--', 'dashed', '-.', 'dashdot', ':', + 'dotted', '', ' ', 'None', 'none'}. Alternatively, the dash tuple + (``offset``, ``onoffseq``) can be specified directly in points. + scale : float + Uniformly scale the internal dash sequence length by a constant + factor. + """ + + self._linestyle_spec = ls + if isinstance(ls, str): + if ls in [' ', '', 'None']: + ls = 'none' + if ls in _ls_mapper: + ls = _ls_mapper[ls] + Enum.__init__(self) + offset, onoffseq = None, None + else: + try: + offset, onoffseq = ls + except ValueError: # not enough/too many values to unpack + raise ValueError('LineStyle should be a string or a 2-tuple, ' + 'instead received: ' + str(ls)) + if offset is None: + _api.warn_deprecated( + "3.3", message="Passing the dash offset as None is deprecated " + "since %(since)s and support for it will be removed " + "%(removal)s; pass it as zero instead.") + offset = 0 + + if onoffseq is not None: + # normalize offset to be positive and shorter than the dash cycle + dsum = sum(onoffseq) + if dsum: + offset %= dsum + if len(onoffseq) % 2 != 0: + raise ValueError('LineStyle onoffseq must be of even length.') + if not all(isinstance(elem, Number) for elem in onoffseq): + raise ValueError('LineStyle onoffseq must be list of floats.') + self._us_offset = offset + self._us_onoffseq = onoffseq + + def __hash__(self): + if self == LineStyle.custom: + return (self._us_offset, tuple(self._us_onoffseq)).__hash__() + return _AutoStringNameEnum.__hash__(self) + + + def get_dashes(self, lw=1): + """ + Get the (scaled) dash sequence for this `.LineStyle`. + """ + # defer lookup until draw time + if self._us_offset is None or self._us_onoffseq is None: + self._us_offset, self._us_onoffseq = \ + LineStyle._get_dash_pattern(self.name) + # normalize offset to be positive and shorter than the dash cycle + dsum = sum(self._us_onoffseq) + self._us_offset %= dsum + return self._scale_dashes(self._us_offset, self._us_onoffseq, lw) + + @staticmethod + def _scale_dashes(offset, dashes, lw): + from . import rcParams + if not rcParams['lines.scale_dashes']: + return offset, dashes + scaled_offset = offset * lw + scaled_dashes = ([x * lw if x is not None else None for x in dashes] + if dashes is not None else None) + return scaled_offset, scaled_dashes + + @staticmethod + def _get_dash_pattern(style): + """Convert linestyle string to explicit dash pattern.""" + # import must be local for validator code to live here + from . import rcParams + # un-dashed styles + if style in ['solid', 'None']: + offset = 0 + dashes = None + # dashed styles + elif style in ['dashed', 'dashdot', 'dotted']: + offset = 0 + dashes = tuple(rcParams['lines.{}_pattern'.format(style)]) + return offset, dashes + + @staticmethod + def from_dashes(seq): + """ + Create a `.LineStyle` from a dash sequence (i.e. the ``onoffseq``). + + The dash sequence is a sequence of floats of even length describing + the length of dashes and spaces in points. + + Parameters + ---------- + seq : sequence of floats (on/off ink in points) or (None, None) + If *seq* is empty or ``(None, None)``, the `.LineStyle` will be + solid. + """ + if seq == (None, None) or len(seq) == 0: + return LineStyle('-') + else: + return LineStyle((0, seq)) + + @staticmethod + def demo(): + import numpy as np + import matplotlib.pyplot as plt + + linestyle_str = [ + ('solid', 'solid'), # Same as (0, ()) or '-' + ('dotted', 'dotted'), # Same as (0, (1, 1)) or '.' + ('dashed', 'dashed'), # Same as '--' + ('dashdot', 'dashdot')] # Same as '-.' + + linestyle_tuple = [ + ('loosely dotted', (0, (1, 10))), + ('dotted', (0, (1, 1))), + ('densely dotted', (0, (1, 1))), + + ('loosely dashed', (0, (5, 10))), + ('dashed', (0, (5, 5))), + ('densely dashed', (0, (5, 1))), + + ('loosely dashdotted', (0, (3, 10, 1, 10))), + ('dashdotted', (0, (3, 5, 1, 5))), + ('densely dashdotted', (0, (3, 1, 1, 1))), + + ('dashdotdotted', (0, (3, 5, 1, 5, 1, 5))), + ('loosely dashdotdotted', (0, (3, 10, 1, 10, 1, 10))), + ('densely dashdotdotted', (0, (3, 1, 1, 1, 1, 1)))] + + def plot_linestyles(ax, linestyles, title): + X, Y = np.linspace(0, 100, 10), np.zeros(10) + yticklabels = [] + + for i, (name, linestyle) in enumerate(linestyles): + ax.plot(X, Y+i, linestyle=linestyle, linewidth=1.5, + color='black') + yticklabels.append(name) + + ax.set_title(title) + ax.set(ylim=(-0.5, len(linestyles)-0.5), + yticks=np.arange(len(linestyles)), + yticklabels=yticklabels) + ax.tick_params(left=False, bottom=False, labelbottom=False) + for spine in ax.spines.values(): + spine.set_visible(False) + + # For each line style, add a text annotation with a small offset + # from the reference point (0 in Axes coords, y tick value in Data + # coords). + for i, (name, linestyle) in enumerate(linestyles): + ax.annotate(repr(linestyle), + xy=(0.0, i), xycoords=ax.get_yaxis_transform(), + xytext=(-6, -12), textcoords='offset points', + color="blue", fontsize=8, ha="right", + family="monospace") + + ax0, ax1 = (plt.figure(figsize=(10, 8)) + .add_gridspec(2, 1, height_ratios=[1, 3]) + .subplots()) + + plot_linestyles(ax0, linestyle_str[::-1], title='Named linestyles') + plot_linestyles(ax1, linestyle_tuple[::-1], + title='Parametrized linestyles') + + plt.tight_layout() + plt.show() + + +LineStyle._ls_mapper = _ls_mapper +LineStyle._deprecated_lineStyles = _deprecated_lineStyles diff --git a/lib/matplotlib/cbook/__init__.py b/lib/matplotlib/cbook/__init__.py index 31055dcd7990..92e72b863ed3 100644 --- a/lib/matplotlib/cbook/__init__.py +++ b/lib/matplotlib/cbook/__init__.py @@ -1266,12 +1266,6 @@ def _compute_conf_interval(data, med, iqr, bootstrap): return bxpstats -#: Maps short codes for line style to their full name used by backends. -ls_mapper = {'-': 'solid', '--': 'dashed', '-.': 'dashdot', ':': 'dotted'} -#: Maps full names for line styles used by backends to their short codes. -ls_mapper_r = {v: k for k, v in ls_mapper.items()} - - def contiguous_regions(mask): """ Return a list of (ind0, ind1) such that ``mask[ind0:ind1].all()`` is diff --git a/lib/matplotlib/lines.py b/lib/matplotlib/lines.py index 41b2f98c314e..6008cafc8929 100644 --- a/lib/matplotlib/lines.py +++ b/lib/matplotlib/lines.py @@ -11,13 +11,12 @@ import matplotlib as mpl from . import _api, artist, cbook, colors as mcolors, docstring, rcParams from .artist import Artist, allow_rasterization -from .cbook import ( - _to_unmasked_float_array, ls_mapper, ls_mapper_r, STEP_LOOKUP_MAP) +from .cbook import _to_unmasked_float_array, STEP_LOOKUP_MAP from .colors import is_color_like, get_named_colors_mapping from .markers import MarkerStyle from .path import Path from .transforms import Bbox, BboxTransformTo, TransformedPath -from ._enums import JoinStyle, CapStyle +from ._enums import JoinStyle, CapStyle, LineStyle # Imported here for backward compatibility, even though they don't # really belong. @@ -30,49 +29,6 @@ _log = logging.getLogger(__name__) -def _get_dash_pattern(style): - """Convert linestyle to dash pattern.""" - # go from short hand -> full strings - if isinstance(style, str): - style = ls_mapper.get(style, style) - # un-dashed styles - if style in ['solid', 'None']: - offset = 0 - dashes = None - # dashed styles - elif style in ['dashed', 'dashdot', 'dotted']: - offset = 0 - dashes = tuple(rcParams['lines.{}_pattern'.format(style)]) - # - elif isinstance(style, tuple): - offset, dashes = style - if offset is None: - _api.warn_deprecated( - "3.3", message="Passing the dash offset as None is deprecated " - "since %(since)s and support for it will be removed " - "%(removal)s; pass it as zero instead.") - offset = 0 - else: - raise ValueError('Unrecognized linestyle: %s' % str(style)) - - # normalize offset to be positive and shorter than the dash cycle - if dashes is not None: - dsum = sum(dashes) - if dsum: - offset %= dsum - - return offset, dashes - - -def _scale_dashes(offset, dashes, lw): - if not rcParams['lines.scale_dashes']: - return offset, dashes - scaled_offset = offset * lw - scaled_dashes = ([x * lw if x is not None else None for x in dashes] - if dashes is not None else None) - return scaled_offset, scaled_dashes - - def segment_hits(cx, cy, x, y, radius): """ Return the indices of the segments in the polyline with coordinates (*cx*, @@ -217,15 +173,7 @@ class Line2D(Artist): can create "stepped" lines in various styles. """ - lineStyles = _lineStyles = { # hidden names deprecated - '-': '_draw_solid', - '--': '_draw_dashed', - '-.': '_draw_dash_dot', - ':': '_draw_dotted', - 'None': '_draw_nothing', - ' ': '_draw_nothing', - '': '_draw_nothing', - } + lineStyles = _lineStyles = LineStyle._deprecated_lineStyles _drawStyles_l = { 'default': '_draw_lines', @@ -303,7 +251,7 @@ def __init__(self, xdata, ydata, %(Line2D_kwdoc)s - See :meth:`set_linestyle` for a description of the line styles, + See `.LineStyle` for a description of the line styles, :meth:`set_marker` for a description of the markers, and :meth:`set_drawstyle` for a description of the draw styles. @@ -320,7 +268,7 @@ def __init__(self, xdata, ydata, linewidth = rcParams['lines.linewidth'] if linestyle is None: - linestyle = rcParams['lines.linestyle'] + linestyle = LineStyle(rcParams['lines.linestyle']) if marker is None: marker = rcParams['lines.marker'] if markerfacecolor is None: @@ -359,16 +307,8 @@ def __init__(self, xdata, ydata, self._drawstyle = None self._linewidth = linewidth - # scaled dash + offset - self._dashSeq = None - self._dashOffset = 0 - # unscaled dash + offset - # this is needed scaling the dash pattern by linewidth - self._us_dashSeq = None - self._us_dashOffset = 0 - - self.set_linewidth(linewidth) self.set_linestyle(linestyle) + self.set_linewidth(linewidth) self.set_drawstyle(drawstyle) self._color = None @@ -792,7 +732,7 @@ def draw(self, renderer): if self.get_sketch_params() is not None: gc.set_sketch_params(*self.get_sketch_params()) - gc.set_dashes(self._dashOffset, self._dashSeq) + gc.set_dashes(*self._linestyle.get_dashes(self._linewidth)) renderer.draw_path(gc, tpath, affine.frozen()) gc.restore() @@ -902,7 +842,7 @@ def get_linestyle(self): See also `~.Line2D.set_linestyle`. """ - return self._linestyle + return self._linestyle._linestyle_spec def get_linewidth(self): """ @@ -1105,57 +1045,32 @@ def set_linewidth(self, w): if self._linewidth != w: self.stale = True self._linewidth = w - # rescale the dashes + offset - self._dashOffset, self._dashSeq = _scale_dashes( - self._us_dashOffset, self._us_dashSeq, self._linewidth) + if rcParams['lines.scale_dashes']: + self._linestyle.scale = self._linewidth def set_linestyle(self, ls): """ - Set the linestyle of the line. + Set the `.LineStyle` of the line. Parameters ---------- - ls : {'-', '--', '-.', ':', '', (offset, on-off-seq), ...} - Possible values: - - - A string: + ls : {'-', '--', ':', '-.', ...} or other `.LineStyle`-like + Set the dashing pattern for the line. Typical values are - =============================== ================= - Linestyle Description - =============================== ================= - ``'-'`` or ``'solid'`` solid line - ``'--'`` or ``'dashed'`` dashed line - ``'-.'`` or ``'dashdot'`` dash-dotted line - ``':'`` or ``'dotted'`` dotted line - ``'None'`` or ``' '`` or ``''`` draw nothing - =============================== ================= + =============================== ================= + Linestyle Description + =============================== ================= + ``'-'`` or ``'solid'`` solid line + ``'--'`` or ``'dashed'`` dashed line + ``'-.'`` or ``'dashdot'`` dash-dotted line + ``':'`` or ``'dotted'`` dotted line + ``'None'`` or ``' '`` or ``''`` draw nothing + =============================== ================= - - Alternatively a dash tuple of the following form can be - provided:: - - (offset, onoffseq) - - where ``onoffseq`` is an even length tuple of on and off ink - in points. See also :meth:`set_dashes`. - - For examples see :doc:`/gallery/lines_bars_and_markers/linestyles`. + For a full description of possible inputs and examples, see the + `.LineStyle` docs. """ - if isinstance(ls, str): - if ls in [' ', '', 'none']: - ls = 'None' - - _api.check_in_list([*self._lineStyles, *ls_mapper_r], ls=ls) - if ls not in self._lineStyles: - ls = ls_mapper_r[ls] - self._linestyle = ls - else: - self._linestyle = '--' - - # get the unscaled dashes - self._us_dashOffset, self._us_dashSeq = _get_dash_pattern(ls) - # compute the linewidth scaled dashes - self._dashOffset, self._dashSeq = _scale_dashes( - self._us_dashOffset, self._us_dashSeq, self._linewidth) + self._linestyle = LineStyle(ls) @docstring.interpd def set_marker(self, marker): @@ -1268,25 +1183,9 @@ def set_ydata(self, y): self.stale = True def set_dashes(self, seq): - """ - Set the dash sequence. + self.set_linestyle(LineStyle.from_dashes(seq)) - The dash sequence is a sequence of floats of even length describing - the length of dashes and spaces in points. - - For example, (5, 2, 1, 2) describes a sequence of 5 point and 1 point - dashes separated by 2 point spaces. - - Parameters - ---------- - seq : sequence of floats (on/off ink in points) or (None, None) - If *seq* is empty or ``(None, None)``, the linestyle will be set - to solid. - """ - if seq == (None, None) or len(seq) == 0: - self.set_linestyle('-') - else: - self.set_linestyle((0, seq)) + set_dashes.__doc__ = LineStyle.from_dashes.__doc__ def update_from(self, other): """Copy properties from *other* to self.""" @@ -1299,10 +1198,6 @@ def update_from(self, other): self._markerfacecoloralt = other._markerfacecoloralt self._markeredgecolor = other._markeredgecolor self._markeredgewidth = other._markeredgewidth - self._dashSeq = other._dashSeq - self._us_dashSeq = other._us_dashSeq - self._dashOffset = other._dashOffset - self._us_dashOffset = other._us_dashOffset self._dashcapstyle = other._dashcapstyle self._dashjoinstyle = other._dashjoinstyle self._solidcapstyle = other._solidcapstyle diff --git a/lib/matplotlib/patches.py b/lib/matplotlib/patches.py index 81df8568f243..60fcacf3a6be 100644 --- a/lib/matplotlib/patches.py +++ b/lib/matplotlib/patches.py @@ -16,7 +16,7 @@ get_parallels, inside_circle, make_wedged_bezier2, split_bezier_intersecting_with_closedpath, split_path_inout) from .path import Path -from ._enums import JoinStyle, CapStyle +from ._enums import JoinStyle, CapStyle, LineStyle @cbook._define_aliases({ @@ -92,8 +92,6 @@ def __init__(self, else: self.set_edgecolor(edgecolor) self.set_facecolor(facecolor) - # unscaled dashes. Needed to scale dash patterns by lw - self._us_dashes = None self._linewidth = 0 self.set_fill(fill) @@ -254,9 +252,8 @@ def update_from(self, other): self._fill = other._fill self._hatch = other._hatch self._hatch_color = other._hatch_color - # copy the unscaled dash pattern - self._us_dashes = other._us_dashes - self.set_linewidth(other._linewidth) # also sets dash properties + self.set_linestyle(other._linestyle) + self.set_linewidth(other._linewidth) self.set_transform(other.get_data_transform()) # If the transform of other needs further initialization, then it will # be the case for this artist too. @@ -308,7 +305,7 @@ def get_linewidth(self): def get_linestyle(self): """Return the linestyle.""" - return self._linestyle + return self._linestyle._linestyle_spec def set_antialiased(self, aa): """ @@ -404,10 +401,6 @@ def set_linewidth(self, w): w = mpl.rcParams['axes.linewidth'] self._linewidth = float(w) - # scale the dash pattern by the linewidth - offset, ls = self._us_dashes - self._dashoffset, self._dashes = mlines._scale_dashes( - offset, ls, self._linewidth) self.stale = True def set_linestyle(self, ls): @@ -438,16 +431,7 @@ def set_linestyle(self, ls): ls : {'-', '--', '-.', ':', '', (offset, on-off-seq), ...} The line style. """ - if ls is None: - ls = "solid" - if ls in [' ', '', 'none']: - ls = 'None' - self._linestyle = ls - # get the unscaled dash pattern - offset, ls = self._us_dashes = mlines._get_dash_pattern(ls) - # scale the dash pattern by the linewidth - self._dashoffset, self._dashes = mlines._scale_dashes( - offset, ls, self._linewidth) + self._linestyle = LineStyle(ls) self.stale = True def set_fill(self, b): @@ -560,10 +544,12 @@ def _bind_draw_path_function(self, renderer): gc.set_foreground(self._edgecolor, isRGBA=True) lw = self._linewidth - if self._edgecolor[3] == 0 or self._linestyle == 'None': + if self._edgecolor[3] == 0 or self.get_linestyle() == 'None': lw = 0 gc.set_linewidth(lw) - gc.set_dashes(self._dashoffset, self._dashes) + dash_offset, onoffseq = self._linestyle.get_dashes(lw) + # Patch has traditionally ignored the dashoffset. + gc.set_dashes(0, onoffseq) gc.set_capstyle(self._capstyle) gc.set_joinstyle(self._joinstyle) @@ -600,9 +586,7 @@ def draw(self, renderer): # docstring inherited if not self.get_visible(): return - # Patch has traditionally ignored the dashoffset. - with cbook._setattr_cm(self, _dashoffset=0), \ - self._bind_draw_path_function(renderer) as draw_path: + with self._bind_draw_path_function(renderer) as draw_path: path = self.get_path() transform = self.get_transform() tpath = transform.transform_path_non_affine(path) diff --git a/lib/matplotlib/rcsetup.py b/lib/matplotlib/rcsetup.py index 535649b03f9f..3b10e6218a54 100644 --- a/lib/matplotlib/rcsetup.py +++ b/lib/matplotlib/rcsetup.py @@ -3,7 +3,7 @@ Matplotlib's rc settings. Each rc setting is assigned a function used to validate any attempted changes -to that setting. The validation functions are defined in the rcsetup module, +to that setting. The validation functions are defined in the rcsetup module, and are used to construct the rcParams global object which stores the settings and is referenced throughout Matplotlib. @@ -23,10 +23,9 @@ import numpy as np from matplotlib import _api, animation, cbook -from matplotlib.cbook import ls_mapper from matplotlib.colors import Colormap, is_color_like from matplotlib.fontconfig_pattern import parse_fontconfig_pattern -from matplotlib._enums import JoinStyle, CapStyle +from matplotlib._enums import JoinStyle, CapStyle, LineStyle # Don't let the original cycler collide with our validating cycler from cycler import Cycler, cycler as ccycler @@ -530,27 +529,18 @@ def validate_ps_distiller(s): # A validator dedicated to the named line styles, based on the items in # ls_mapper, and a list of possible strings read from Line2D.set_linestyle -_validate_named_linestyle = ValidateInStrings( - 'linestyle', - [*ls_mapper.keys(), *ls_mapper.values(), 'None', 'none', ' ', ''], - ignorecase=True) - - def _validate_linestyle(ls): """ A validator for all possible line styles, the named ones *and* the on-off ink sequences. """ if isinstance(ls, str): - try: # Look first for a valid named line style, like '--' or 'solid'. - return _validate_named_linestyle(ls) - except ValueError: - pass try: ls = ast.literal_eval(ls) # Parsing matplotlibrc. except (SyntaxError, ValueError): pass # Will error with the ValueError at the end. +<<<<<<< HEAD def _is_iterable_not_string_like(x): # Explicitly exclude bytes/bytearrays so that they are not # nonsensically interpreted as sequences of numbers (codepoints). @@ -576,6 +566,21 @@ def _is_iterable_not_string_like(x): and all(isinstance(elem, Number) for elem in ls)): return (0, ls) raise ValueError(f"linestyle {ls!r} is not a valid on-off ink sequence.") +======= + try: + LineStyle(ls) + except ValueError as e: + # For backcompat, only in rc, we allow the user to pash only the + # onoffseq, and set the offset implicitly to 0 + if (np.iterable(ls) and not isinstance(ls, (str, bytes, bytearray)) + and len(ls) % 2 == 0 + and all(isinstance(elem, Number) for elem in ls)): + try: + LineStyle((0, ls)) + except ValueError: + raise e + return ls +>>>>>>> b2d2793cc... GSOD: LineStyle class validate_fillstyle = ValidateInStrings( From 451ca5a77dc5b9c44cdafbb2512ae493481d4f82 Mon Sep 17 00:00:00 2001 From: Bruno Beltran Date: Mon, 4 Jan 2021 12:03:33 -0800 Subject: [PATCH 2/3] META: aliasable _enums and some LineStyle bugfixes --- lib/matplotlib/_api/__init__.py | 4 + lib/matplotlib/_enums.py | 225 ++++++++++++++++++++++---------- lib/matplotlib/rcsetup.py | 45 +------ 3 files changed, 164 insertions(+), 110 deletions(-) diff --git a/lib/matplotlib/_api/__init__.py b/lib/matplotlib/_api/__init__.py index f251e07bab59..cbf6800913f6 100644 --- a/lib/matplotlib/_api/__init__.py +++ b/lib/matplotlib/_api/__init__.py @@ -211,3 +211,7 @@ def warn_external(message, category=None): break frame = frame.f_back warnings.warn(message, category, stacklevel) + + +def is_string_like(x): + return isinstance(x, (str, bytes, bytearray)) diff --git a/lib/matplotlib/_enums.py b/lib/matplotlib/_enums.py index 594b70f381e3..c690cebb12a6 100644 --- a/lib/matplotlib/_enums.py +++ b/lib/matplotlib/_enums.py @@ -10,17 +10,81 @@ they define. """ -from enum import Enum, auto -from numbers import Number +from enum import _EnumDict, EnumMeta, Enum, auto + +import numpy as np from matplotlib import _api, docstring -class _AutoStringNameEnum(Enum): - """Automate the ``name = 'name'`` part of making a (str, Enum).""" +class _AliasableStrEnumDict(_EnumDict): + """Helper for `_AliasableEnumMeta`.""" + def __init__(self): + super().__init__() + self._aliases = {} + # adopt the Python 3.10 convention of "auto()" simply using the name of + # the attribute: https://bugs.python.org/issue42385 + # this can be removed once we no longer support Python 3.9 + self._generate_next_value \ + = lambda name, start, count, last_values: name + + def __setitem__(self, key, value): + # if a class attribute with this name has already been created, + # register this as an "alias" + if key in self: + self._aliases[value] = self[key] + else: + super().__setitem__(key, value) - def _generate_next_value_(name, start, count, last_values): - return name + +class _AliasableEnumMeta(EnumMeta): + """ + Allow Enums to have multiple "values" which are equivalent. + + For a discussion of several approaches to "value aliasing", see + https://stackoverflow.com/questions/24105268/is-it-possible-to-override-new-in-an-enum-to-parse-strings-to-an-instance + """ + @classmethod + def __prepare__(metacls, cls, bases): + # a custom dict (_EnumDict) is used when handing the __prepared__ + # class's namespace to EnumMeta.__new__. This way, when non-dunder, + # non-descriptor class-level variables are added to the class namespace + # during class-body execution, their values can be replaced with the + # singletons that will later be returned by Enum.__call__. + + # We over-ride this dict to prevent _EnumDict's internal checks from + # throwing an error whenever preventing the same name is inserted + # twice. Instead, we add that name to a _aliases dict that can be + # used to look up the correct singleton later. + return _AliasableStrEnumDict() + + def __new__(metacls, cls, bases, classdict): + # add our _aliases dict to the newly created class, so that it + # can be used by __call__. + enum_class = super().__new__(metacls, cls, bases, classdict) + enum_class._aliases_ = classdict._aliases + return enum_class + + def __call__(cls, value, *args, **kw): + # convert the value to the "default" if it is an alias, and then simply + # forward to Enum + if value not in cls. _value2member_map_ and value in cls._aliases_: + value = cls._aliases_[value] + return super().__call__(value, *args, **kw) + + +class _AliasableStringNameEnum(Enum, metaclass=_AliasableEnumMeta): + """ + Convenience mix-in for easier construction of string enums. + + Automates the ``name = 'name'`` part of making a (str, Enum), using the + semantics that have now been adopted as part of Python 3.10: + (bugs.python.org/issue42385). + + In addition, allow multiple strings to be synonyms for the same underlying + Enum value. This allows us to easily have things like ``LineStyle('--') == + LineStyle('dashed')`` work as expected. + """ def __hash__(self): return str(self).__hash__() @@ -43,7 +107,7 @@ def _deprecate_case_insensitive_join_cap(s): return s_low -class JoinStyle(str, _AutoStringNameEnum): +class JoinStyle(str, _AliasableStringNameEnum): """ Define how the connection between two line segments is drawn. @@ -139,7 +203,7 @@ def plot_angle(ax, x, y, angle, style): + "}" -class CapStyle(str, _AutoStringNameEnum): +class CapStyle(str, _AliasableStringNameEnum): r""" Define how the two endpoints (caps) of an unclosed line are drawn. @@ -211,7 +275,7 @@ def demo(): #: Maps short codes for line style to their full name used by backends. -_ls_mapper = {'': 'None', ' ': 'None', 'none': 'None', +_ls_mapper = {'': 'none', ' ': 'none', 'none': 'none', '-': 'solid', '--': 'dashed', '-.': 'dashdot', ':': 'dotted'} _deprecated_lineStyles = { '-': '_draw_solid', @@ -224,7 +288,37 @@ def demo(): } -class NamedLineStyle(str, _AutoStringNameEnum): +def _validate_onoffseq(x): + """Raise a helpful error message for malformed onoffseq.""" + err = 'In a custom LineStyle (offset, onoffseq), the onoffseq must ' + if _api.is_string_like(x): + raise ValueError(err + 'not be a string.') + if not np.iterable(x): + raise ValueError(err + 'be iterable.') + if not len(x) % 2 == 0: + raise ValueError(err + 'be of even length.') + if not np.all(x > 0): + raise ValueError(err + 'have strictly positive, numerical elements.') + + +class _NamedLineStyle(_AliasableStringNameEnum): + """A standardized way to refer to each named LineStyle internally.""" + solid = auto() + solid = '-' + dashed = auto() + dashed = '--' + dotted = auto() + dotted = ':' + dashdot = auto() + dashdot = '-.' + none = auto() + none = 'None' + none = ' ' + none = '' + custom = auto() + + +class LineStyle: """ Describe if the line is solid or dashed, and the dash pattern, if any. @@ -239,7 +333,7 @@ class NamedLineStyle(str, _AutoStringNameEnum): ``'--'`` or ``'dashed'`` dashed line ``'-.'`` or ``'dashdot'`` dash-dotted line ``':'`` or ``'dotted'`` dotted line - ``'None'`` or ``' '`` or ``''`` draw nothing + ``'none'`` or ``' '`` or ``''`` draw nothing =============================== ================= However, for more fine-grained control, one can directly specify the @@ -249,18 +343,17 @@ class NamedLineStyle(str, _AutoStringNameEnum): where ``onoffseq`` is an even length tuple specifying the lengths of each subsequent dash and space, and ``offset`` controls at which point in this - pattern the start of the line will begin (to allow you to e.g. prevent - corners from happening to land in between dashes). - - For example, (5, 2, 1, 2) describes a sequence of 5 point and 1 point - dashes separated by 2 point spaces. + pattern the start of the line will begin (allowing you to, for example, + prevent a sharp corner landing in between dashes and therefore not being + drawn). - Setting ``onoffseq`` to ``None`` results in a solid *LineStyle*. + For example, the ``onoffseq`` (5, 2, 1, 2) describes a sequence of 5 point + and 1 point dashes separated by 2 point spaces. The default dashing patterns described in the table above are themselves - all described in this notation, and can therefore be customized by editing - the appropriate ``lines.*_pattern`` *rc* parameter, as described in - :doc:`/tutorials/introductory/customizing`. + defined under the hood using an offset and an onoffseq, and can therefore + be customized by editing the appropriate ``lines.*_pattern`` *rc* + parameter, as described in :doc:`/tutorials/introductory/customizing`. .. plot:: :alt: Demo of possible LineStyle's. @@ -271,22 +364,15 @@ class NamedLineStyle(str, _AutoStringNameEnum): .. note:: In addition to directly taking a ``linestyle`` argument, - `~.lines.Line2D` exposes a ``~.lines.Line2D.set_dashes`` method that - can be used to create a new *LineStyle* by providing just the - ``onoffseq``, but does not let you customize the offset. This method is - called when using the keyword *dashes* to the cycler , as shown in - :doc:`property_cycle `. + `~.lines.Line2D` exposes a ``~.lines.Line2D.set_dashes`` method (and + the :doc:`property_cycle ` has a + *dashes* keyword) that can be used to create a new *LineStyle* by + providing just the ``onoffseq``, but does not let you customize the + offset. This method simply sets the underlying linestyle, and is only + kept for backwards compatibility. """ - solid = auto() - dashed = auto() - dotted = auto() - dashdot = auto() - none = auto() - custom = auto() -class LineStyle(str): - - def __init__(self, ls, scale=1): + def __init__(self, ls): """ Parameters ---------- @@ -301,56 +387,58 @@ def __init__(self, ls, scale=1): """ self._linestyle_spec = ls - if isinstance(ls, str): - if ls in [' ', '', 'None']: - ls = 'none' - if ls in _ls_mapper: - ls = _ls_mapper[ls] - Enum.__init__(self) - offset, onoffseq = None, None + if _api.is_string_like(ls): + self._name = _NamedLineStyle(ls) + self._offset, self._onoffseq = None, None else: + self._name = _NamedLineStyle('custom') try: - offset, onoffseq = ls + self._offset, self._onoffseq = ls except ValueError: # not enough/too many values to unpack - raise ValueError('LineStyle should be a string or a 2-tuple, ' - 'instead received: ' + str(ls)) - if offset is None: + raise ValueError('Custom LineStyle must be a 2-tuple (offset, ' + 'onoffseq), instead received: ' + str(ls)) + _validate_onoffseq(self._onoffseq) + if self._offset is None: _api.warn_deprecated( "3.3", message="Passing the dash offset as None is deprecated " "since %(since)s and support for it will be removed " "%(removal)s; pass it as zero instead.") - offset = 0 + self._offset = 0 - if onoffseq is not None: - # normalize offset to be positive and shorter than the dash cycle - dsum = sum(onoffseq) - if dsum: - offset %= dsum - if len(onoffseq) % 2 != 0: - raise ValueError('LineStyle onoffseq must be of even length.') - if not all(isinstance(elem, Number) for elem in onoffseq): - raise ValueError('LineStyle onoffseq must be list of floats.') - self._us_offset = offset - self._us_onoffseq = onoffseq + def __eq__(self, other): + if not isinstance(other, LineStyle): + other = LineStyle(other) + return self.get_dashes() == other.get_dashes() def __hash__(self): - if self == LineStyle.custom: - return (self._us_offset, tuple(self._us_onoffseq)).__hash__() - return _AutoStringNameEnum.__hash__(self) + if self._name == LineStyle.custom: + return (self._offset, tuple(self._onoffseq)).__hash__() + return _AliasableStringNameEnum.__hash__(self._name) + @staticmethod + def _normalize_offset(offset, onoffseq): + """Normalize offset to be positive and shorter than the dash cycle.""" + dsum = sum(onoffseq) + if dsum: + offset %= dsum + return offset + + def is_dashed(self): + offset, onoffseq = self.get_dashes() + return np.isclose(np.sum(onoffseq), 0) def get_dashes(self, lw=1): """ Get the (scaled) dash sequence for this `.LineStyle`. """ - # defer lookup until draw time - if self._us_offset is None or self._us_onoffseq is None: - self._us_offset, self._us_onoffseq = \ - LineStyle._get_dash_pattern(self.name) - # normalize offset to be positive and shorter than the dash cycle - dsum = sum(self._us_onoffseq) - self._us_offset %= dsum - return self._scale_dashes(self._us_offset, self._us_onoffseq, lw) + # named linestyle lookup happens at draw time (here) + if self._onoffseq is None: + offset, onoffseq = LineStyle._get_dash_pattern(self._name) + else: + offset, onoff_seq = self._offset, self._onoffseq + # force 0 <= offset < dash cycle length + offset = LineStyle._normalize_offset(offset, onoffseq) + return self._scale_dashes(offset, onoffseq, lw) @staticmethod def _scale_dashes(offset, dashes, lw): @@ -462,6 +550,5 @@ def plot_linestyles(ax, linestyles, title): plt.tight_layout() plt.show() - LineStyle._ls_mapper = _ls_mapper LineStyle._deprecated_lineStyles = _deprecated_lineStyles diff --git a/lib/matplotlib/rcsetup.py b/lib/matplotlib/rcsetup.py index 3b10e6218a54..e601e279fde4 100644 --- a/lib/matplotlib/rcsetup.py +++ b/lib/matplotlib/rcsetup.py @@ -16,7 +16,6 @@ import ast from functools import lru_cache, reduce import logging -from numbers import Number import operator import re @@ -527,8 +526,6 @@ def validate_ps_distiller(s): return ValidateInStrings('ps.usedistiller', ['ghostscript', 'xpdf'])(s) -# A validator dedicated to the named line styles, based on the items in -# ls_mapper, and a list of possible strings read from Line2D.set_linestyle def _validate_linestyle(ls): """ A validator for all possible line styles, the named ones *and* @@ -539,48 +536,14 @@ def _validate_linestyle(ls): ls = ast.literal_eval(ls) # Parsing matplotlibrc. except (SyntaxError, ValueError): pass # Will error with the ValueError at the end. - -<<<<<<< HEAD - def _is_iterable_not_string_like(x): - # Explicitly exclude bytes/bytearrays so that they are not - # nonsensically interpreted as sequences of numbers (codepoints). - return np.iterable(x) and not isinstance(x, (str, bytes, bytearray)) - - # (offset, (on, off, on, off, ...)) - if (_is_iterable_not_string_like(ls) - and len(ls) == 2 - and isinstance(ls[0], (type(None), Number)) - and _is_iterable_not_string_like(ls[1]) - and len(ls[1]) % 2 == 0 - and all(isinstance(elem, Number) for elem in ls[1])): - if ls[0] is None: - _api.warn_deprecated( - "3.3", message="Passing the dash offset as None is deprecated " - "since %(since)s and support for it will be removed " - "%(removal)s; pass it as zero instead.") - ls = (0, ls[1]) - return ls - # For backcompat: (on, off, on, off, ...); the offset is implicitly None. - if (_is_iterable_not_string_like(ls) - and len(ls) % 2 == 0 - and all(isinstance(elem, Number) for elem in ls)): - return (0, ls) - raise ValueError(f"linestyle {ls!r} is not a valid on-off ink sequence.") -======= try: LineStyle(ls) except ValueError as e: - # For backcompat, only in rc, we allow the user to pash only the - # onoffseq, and set the offset implicitly to 0 - if (np.iterable(ls) and not isinstance(ls, (str, bytes, bytearray)) - and len(ls) % 2 == 0 - and all(isinstance(elem, Number) for elem in ls)): - try: - LineStyle((0, ls)) - except ValueError: - raise e + try: + LineStyle((0, ls)) + except ValueError: + raise e return ls ->>>>>>> b2d2793cc... GSOD: LineStyle class validate_fillstyle = ValidateInStrings( From 3b8a30feab307e08ad1d9e8d3a5a0a9408100700 Mon Sep 17 00:00:00 2001 From: Bruno Beltran Date: Mon, 8 Feb 2021 12:20:15 -0800 Subject: [PATCH 3/3] BF: LineStyle eq and hash fixes --- lib/matplotlib/_enums.py | 41 +++++++++++++++++++++++++--------------- 1 file changed, 26 insertions(+), 15 deletions(-) diff --git a/lib/matplotlib/_enums.py b/lib/matplotlib/_enums.py index c690cebb12a6..c4bd5cd098fd 100644 --- a/lib/matplotlib/_enums.py +++ b/lib/matplotlib/_enums.py @@ -301,7 +301,7 @@ def _validate_onoffseq(x): raise ValueError(err + 'have strictly positive, numerical elements.') -class _NamedLineStyle(_AliasableStringNameEnum): +class _NamedLineStyle(str, _AliasableStringNameEnum): """A standardized way to refer to each named LineStyle internally.""" solid = auto() solid = '-' @@ -388,10 +388,10 @@ def __init__(self, ls): self._linestyle_spec = ls if _api.is_string_like(ls): - self._name = _NamedLineStyle(ls) - self._offset, self._onoffseq = None, None + self._named = _NamedLineStyle(ls) + self._offset, self._onoffseq = 0, None else: - self._name = _NamedLineStyle('custom') + self._named = _NamedLineStyle('custom') try: self._offset, self._onoffseq = ls except ValueError: # not enough/too many values to unpack @@ -400,9 +400,9 @@ def __init__(self, ls): _validate_onoffseq(self._onoffseq) if self._offset is None: _api.warn_deprecated( - "3.3", message="Passing the dash offset as None is deprecated " - "since %(since)s and support for it will be removed " - "%(removal)s; pass it as zero instead.") + "3.3", message="Passing the dash offset as None is " + "deprecated since %(since)s and support for it will be " + "removed %(removal)s; pass it as zero instead.") self._offset = 0 def __eq__(self, other): @@ -411,13 +411,19 @@ def __eq__(self, other): return self.get_dashes() == other.get_dashes() def __hash__(self): - if self._name == LineStyle.custom: + if self._named == 'custom': return (self._offset, tuple(self._onoffseq)).__hash__() - return _AliasableStringNameEnum.__hash__(self._name) + return self._named.__hash__() + + def __repr__(self): + return self._named.__repr__() + ' with (offset, onoffseq) = ' \ + + str(self.get_dashes()) @staticmethod def _normalize_offset(offset, onoffseq): """Normalize offset to be positive and shorter than the dash cycle.""" + if onoffseq is None: + return 0 dsum = sum(onoffseq) if dsum: offset %= dsum @@ -425,17 +431,18 @@ def _normalize_offset(offset, onoffseq): def is_dashed(self): offset, onoffseq = self.get_dashes() - return np.isclose(np.sum(onoffseq), 0) + total_dash_length = np.sum(onoffseq) + return total_dash_length is None or np.isclose(total_dash_length, 0) def get_dashes(self, lw=1): """ Get the (scaled) dash sequence for this `.LineStyle`. """ - # named linestyle lookup happens at draw time (here) - if self._onoffseq is None: - offset, onoffseq = LineStyle._get_dash_pattern(self._name) + # named linestyle lookup happens each time dashes are requested + if self._named != 'custom': + offset, onoffseq = LineStyle._get_named_pattern(self._named) else: - offset, onoff_seq = self._offset, self._onoffseq + offset, onoffseq = self._offset, self._onoffseq # force 0 <= offset < dash cycle length offset = LineStyle._normalize_offset(offset, onoffseq) return self._scale_dashes(offset, onoffseq, lw) @@ -451,7 +458,7 @@ def _scale_dashes(offset, dashes, lw): return scaled_offset, scaled_dashes @staticmethod - def _get_dash_pattern(style): + def _get_named_pattern(style): """Convert linestyle string to explicit dash pattern.""" # import must be local for validator code to live here from . import rcParams @@ -463,6 +470,10 @@ def _get_dash_pattern(style): elif style in ['dashed', 'dashdot', 'dotted']: offset = 0 dashes = tuple(rcParams['lines.{}_pattern'.format(style)]) + else: + raise ValueError("Attempted to get dash pattern from RC for " + "unknown dash name. Allowed values are 'dashed', " + "'dashdot', and 'dotted'.") return offset, dashes @staticmethod