Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
GSOD: LineStyle class
  • Loading branch information
brunobeltran committed Feb 1, 2021
commit 8175daaa4fbd8b318a770691137a0ecf73dc1439
92 changes: 23 additions & 69 deletions examples/lines_bars_and_markers/linestyles.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 </tutorials/intermediate/color_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 <mpl._enums.LineStyle>`.

*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 </tutorials/intermediate/color_cycle>`). This interface is
strictly less expressive, and we recommend using LineStyle (or the keyword
*linestyle* to the :doc:`property cycler
</tutorials/intermediate/color_cycle>`).
"""
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()
265 changes: 262 additions & 3 deletions lib/matplotlib/_enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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.")
Expand Down Expand Up @@ -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 </tutorials/intermediate/color_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
6 changes: 0 additions & 6 deletions lib/matplotlib/cbook/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading