10
10
they define.
11
11
"""
12
12
13
- from enum import Enum , auto
14
- from numbers import Number
13
+ from enum import _EnumDict , EnumMeta , Enum , auto
14
+
15
+ import numpy as np
15
16
16
17
from matplotlib import _api , docstring
17
18
18
19
19
- class _AutoStringNameEnum (Enum ):
20
- """Automate the ``name = 'name'`` part of making a (str, Enum)."""
20
+ class _AliasableStrEnumDict (_EnumDict ):
21
+ """Helper for `_AliasableEnumMeta`."""
22
+ def __init__ (self ):
23
+ super ().__init__ ()
24
+ self ._aliases = {}
25
+ # adopt the Python 3.10 convention of "auto()" simply using the name of
26
+ # the attribute: https://bugs.python.org/issue42385
27
+ # this can be removed once we no longer support Python 3.9
28
+ self ._generate_next_value \
29
+ = lambda name , start , count , last_values : name
30
+
31
+ def __setitem__ (self , key , value ):
32
+ # if a class attribute with this name has already been created,
33
+ # register this as an "alias"
34
+ if key in self :
35
+ self ._aliases [value ] = self [key ]
36
+ else :
37
+ super ().__setitem__ (key , value )
21
38
22
- def _generate_next_value_ (name , start , count , last_values ):
23
- return name
39
+
40
+ class _AliasableEnumMeta (EnumMeta ):
41
+ """
42
+ Allow Enums to have multiple "values" which are equivalent.
43
+
44
+ For a discussion of several approaches to "value aliasing", see
45
+ https://stackoverflow.com/questions/24105268/is-it-possible-to-override-new-in-an-enum-to-parse-strings-to-an-instance
46
+ """
47
+ @classmethod
48
+ def __prepare__ (metacls , cls , bases ):
49
+ # a custom dict (_EnumDict) is used when handing the __prepared__
50
+ # class's namespace to EnumMeta.__new__. This way, when non-dunder,
51
+ # non-descriptor class-level variables are added to the class namespace
52
+ # during class-body execution, their values can be replaced with the
53
+ # singletons that will later be returned by Enum.__call__.
54
+
55
+ # We over-ride this dict to prevent _EnumDict's internal checks from
56
+ # throwing an error whenever preventing the same name is inserted
57
+ # twice. Instead, we add that name to a _aliases dict that can be
58
+ # used to look up the correct singleton later.
59
+ return _AliasableStrEnumDict ()
60
+
61
+ def __new__ (metacls , cls , bases , classdict ):
62
+ # add our _aliases dict to the newly created class, so that it
63
+ # can be used by __call__.
64
+ enum_class = super ().__new__ (metacls , cls , bases , classdict )
65
+ enum_class ._aliases_ = classdict ._aliases
66
+ return enum_class
67
+
68
+ def __call__ (cls , value , * args , ** kw ):
69
+ # convert the value to the "default" if it is an alias, and then simply
70
+ # forward to Enum
71
+ if value not in cls . _value2member_map_ and value in cls ._aliases_ :
72
+ value = cls ._aliases_ [value ]
73
+ return super ().__call__ (value , * args , ** kw )
74
+
75
+
76
+ class _AliasableStringNameEnum (Enum , metaclass = _AliasableEnumMeta ):
77
+ """
78
+ Convenience mix-in for easier construction of string enums.
79
+
80
+ Automates the ``name = 'name'`` part of making a (str, Enum), using the
81
+ semantics that have now been adopted as part of Python 3.10:
82
+ (bugs.python.org/issue42385).
83
+
84
+ In addition, allow multiple strings to be synonyms for the same underlying
85
+ Enum value. This allows us to easily have things like ``LineStyle('--') ==
86
+ LineStyle('dashed')`` work as expected.
87
+ """
24
88
25
89
def __hash__ (self ):
26
90
return str (self ).__hash__ ()
@@ -43,7 +107,7 @@ def _deprecate_case_insensitive_join_cap(s):
43
107
return s_low
44
108
45
109
46
- class JoinStyle (str , _AutoStringNameEnum ):
110
+ class JoinStyle (str , _AliasableStringNameEnum ):
47
111
"""
48
112
Define how the connection between two line segments is drawn.
49
113
@@ -139,7 +203,7 @@ def plot_angle(ax, x, y, angle, style):
139
203
+ "}"
140
204
141
205
142
- class CapStyle (str , _AutoStringNameEnum ):
206
+ class CapStyle (str , _AliasableStringNameEnum ):
143
207
r"""
144
208
Define how the two endpoints (caps) of an unclosed line are drawn.
145
209
@@ -211,7 +275,7 @@ def demo():
211
275
212
276
213
277
#: Maps short codes for line style to their full name used by backends.
214
- _ls_mapper = {'' : 'None ' , ' ' : 'None ' , 'none' : 'None ' ,
278
+ _ls_mapper = {'' : 'none ' , ' ' : 'none ' , 'none' : 'none ' ,
215
279
'-' : 'solid' , '--' : 'dashed' , '-.' : 'dashdot' , ':' : 'dotted' }
216
280
_deprecated_lineStyles = {
217
281
'-' : '_draw_solid' ,
@@ -224,7 +288,37 @@ def demo():
224
288
}
225
289
226
290
227
- class NamedLineStyle (str , _AutoStringNameEnum ):
291
+ def _validate_onoffseq (x ):
292
+ """Raise a helpful error message for malformed onoffseq."""
293
+ err = 'In a custom LineStyle (offset, onoffseq), the onoffseq must '
294
+ if _api .is_string_like (x ):
295
+ raise ValueError (err + 'not be a string.' )
296
+ if not np .iterable (x ):
297
+ raise ValueError (err + 'be iterable.' )
298
+ if not len (x ) % 2 == 0 :
299
+ raise ValueError (err + 'be of even length.' )
300
+ if not np .all (x > 0 ):
301
+ raise ValueError (err + 'have strictly positive, numerical elements.' )
302
+
303
+
304
+ class _NamedLineStyle (_AliasableStringNameEnum ):
305
+ """A standardized way to refer to each named LineStyle internally."""
306
+ solid = auto ()
307
+ solid = '-'
308
+ dashed = auto ()
309
+ dashed = '--'
310
+ dotted = auto ()
311
+ dotted = ':'
312
+ dashdot = auto ()
313
+ dashdot = '-.'
314
+ none = auto ()
315
+ none = 'None'
316
+ none = ' '
317
+ none = ''
318
+ custom = auto ()
319
+
320
+
321
+ class LineStyle :
228
322
"""
229
323
Describe if the line is solid or dashed, and the dash pattern, if any.
230
324
@@ -239,7 +333,7 @@ class NamedLineStyle(str, _AutoStringNameEnum):
239
333
``'--'`` or ``'dashed'`` dashed line
240
334
``'-.'`` or ``'dashdot'`` dash-dotted line
241
335
``':'`` or ``'dotted'`` dotted line
242
- ``'None '`` or ``' '`` or ``''`` draw nothing
336
+ ``'none '`` or ``' '`` or ``''`` draw nothing
243
337
=============================== =================
244
338
245
339
However, for more fine-grained control, one can directly specify the
@@ -249,18 +343,17 @@ class NamedLineStyle(str, _AutoStringNameEnum):
249
343
250
344
where ``onoffseq`` is an even length tuple specifying the lengths of each
251
345
subsequent dash and space, and ``offset`` controls at which point in this
252
- pattern the start of the line will begin (to allow you to e.g. prevent
253
- corners from happening to land in between dashes).
254
-
255
- For example, (5, 2, 1, 2) describes a sequence of 5 point and 1 point
256
- dashes separated by 2 point spaces.
346
+ pattern the start of the line will begin (allowing you to, for example,
347
+ prevent a sharp corner landing in between dashes and therefore not being
348
+ drawn).
257
349
258
- Setting ``onoffseq`` to ``None`` results in a solid *LineStyle*.
350
+ For example, the ``onoffseq`` (5, 2, 1, 2) describes a sequence of 5 point
351
+ and 1 point dashes separated by 2 point spaces.
259
352
260
353
The default dashing patterns described in the table above are themselves
261
- all described in this notation, and can therefore be customized by editing
262
- the appropriate ``lines.*_pattern`` *rc* parameter, as described in
263
- :doc:`/tutorials/introductory/customizing`.
354
+ defined under the hood using an offset and an onoffseq, and can therefore
355
+ be customized by editing the appropriate ``lines.*_pattern`` *rc*
356
+ parameter, as described in :doc:`/tutorials/introductory/customizing`.
264
357
265
358
.. plot::
266
359
:alt: Demo of possible LineStyle's.
@@ -271,22 +364,15 @@ class NamedLineStyle(str, _AutoStringNameEnum):
271
364
.. note::
272
365
273
366
In addition to directly taking a ``linestyle`` argument,
274
- `~.lines.Line2D` exposes a ``~.lines.Line2D.set_dashes`` method that
275
- can be used to create a new *LineStyle* by providing just the
276
- ``onoffseq``, but does not let you customize the offset. This method is
277
- called when using the keyword *dashes* to the cycler , as shown in
278
- :doc:`property_cycle </tutorials/intermediate/color_cycle>`.
367
+ `~.lines.Line2D` exposes a ``~.lines.Line2D.set_dashes`` method (and
368
+ the :doc:`property_cycle </tutorials/intermediate/color_cycle>` has a
369
+ *dashes* keyword) that can be used to create a new *LineStyle* by
370
+ providing just the ``onoffseq``, but does not let you customize the
371
+ offset. This method simply sets the underlying linestyle, and is only
372
+ kept for backwards compatibility.
279
373
"""
280
- solid = auto ()
281
- dashed = auto ()
282
- dotted = auto ()
283
- dashdot = auto ()
284
- none = auto ()
285
- custom = auto ()
286
374
287
- class LineStyle (str ):
288
-
289
- def __init__ (self , ls , scale = 1 ):
375
+ def __init__ (self , ls ):
290
376
"""
291
377
Parameters
292
378
----------
@@ -301,56 +387,58 @@ def __init__(self, ls, scale=1):
301
387
"""
302
388
303
389
self ._linestyle_spec = ls
304
- if isinstance (ls , str ):
305
- if ls in [' ' , '' , 'None' ]:
306
- ls = 'none'
307
- if ls in _ls_mapper :
308
- ls = _ls_mapper [ls ]
309
- Enum .__init__ (self )
310
- offset , onoffseq = None , None
390
+ if _api .is_string_like (ls ):
391
+ self ._name = _NamedLineStyle (ls )
392
+ self ._offset , self ._onoffseq = None , None
311
393
else :
394
+ self ._name = _NamedLineStyle ('custom' )
312
395
try :
313
- offset , onoffseq = ls
396
+ self . _offset , self . _onoffseq = ls
314
397
except ValueError : # not enough/too many values to unpack
315
- raise ValueError ('LineStyle should be a string or a 2-tuple, '
316
- 'instead received: ' + str (ls ))
317
- if offset is None :
398
+ raise ValueError ('Custom LineStyle must be a 2-tuple (offset, '
399
+ 'onoffseq), instead received: ' + str (ls ))
400
+ _validate_onoffseq (self ._onoffseq )
401
+ if self ._offset is None :
318
402
_api .warn_deprecated (
319
403
"3.3" , message = "Passing the dash offset as None is deprecated "
320
404
"since %(since)s and support for it will be removed "
321
405
"%(removal)s; pass it as zero instead." )
322
- offset = 0
406
+ self . _offset = 0
323
407
324
- if onoffseq is not None :
325
- # normalize offset to be positive and shorter than the dash cycle
326
- dsum = sum (onoffseq )
327
- if dsum :
328
- offset %= dsum
329
- if len (onoffseq ) % 2 != 0 :
330
- raise ValueError ('LineStyle onoffseq must be of even length.' )
331
- if not all (isinstance (elem , Number ) for elem in onoffseq ):
332
- raise ValueError ('LineStyle onoffseq must be list of floats.' )
333
- self ._us_offset = offset
334
- self ._us_onoffseq = onoffseq
408
+ def __eq__ (self , other ):
409
+ if not isinstance (other , LineStyle ):
410
+ other = LineStyle (other )
411
+ return self .get_dashes () == other .get_dashes ()
335
412
336
413
def __hash__ (self ):
337
- if self == LineStyle .custom :
338
- return (self ._us_offset , tuple (self ._us_onoffseq )).__hash__ ()
339
- return _AutoStringNameEnum .__hash__ (self )
414
+ if self . _name == LineStyle .custom :
415
+ return (self ._offset , tuple (self ._onoffseq )).__hash__ ()
416
+ return _AliasableStringNameEnum .__hash__ (self . _name )
340
417
418
+ @staticmethod
419
+ def _normalize_offset (offset , onoffseq ):
420
+ """Normalize offset to be positive and shorter than the dash cycle."""
421
+ dsum = sum (onoffseq )
422
+ if dsum :
423
+ offset %= dsum
424
+ return offset
425
+
426
+ def is_dashed (self ):
427
+ offset , onoffseq = self .get_dashes ()
428
+ return np .isclose (np .sum (onoffseq ), 0 )
341
429
342
430
def get_dashes (self , lw = 1 ):
343
431
"""
344
432
Get the (scaled) dash sequence for this `.LineStyle`.
345
433
"""
346
- # defer lookup until draw time
347
- if self ._us_offset is None or self . _us_onoffseq is None :
348
- self . _us_offset , self . _us_onoffseq = \
349
- LineStyle . _get_dash_pattern ( self . name )
350
- # normalize offset to be positive and shorter than the dash cycle
351
- dsum = sum ( self . _us_onoffseq )
352
- self . _us_offset %= dsum
353
- return self ._scale_dashes (self . _us_offset , self . _us_onoffseq , lw )
434
+ # named linestyle lookup happens at draw time (here)
435
+ if self ._onoffseq is None :
436
+ offset , onoffseq = LineStyle . _get_dash_pattern ( self . _name )
437
+ else :
438
+ offset , onoff_seq = self . _offset , self . _onoffseq
439
+ # force 0 <= offset < dash cycle length
440
+ offset = LineStyle . _normalize_offset ( offset , onoffseq )
441
+ return self ._scale_dashes (offset , onoffseq , lw )
354
442
355
443
@staticmethod
356
444
def _scale_dashes (offset , dashes , lw ):
@@ -462,6 +550,5 @@ def plot_linestyles(ax, linestyles, title):
462
550
plt .tight_layout ()
463
551
plt .show ()
464
552
465
-
466
553
LineStyle ._ls_mapper = _ls_mapper
467
554
LineStyle ._deprecated_lineStyles = _deprecated_lineStyles
0 commit comments