-
Notifications
You must be signed in to change notification settings - Fork 96
Expand file tree
/
Copy pathutils.py
More file actions
905 lines (780 loc) · 24.7 KB
/
utils.py
File metadata and controls
905 lines (780 loc) · 24.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
#!/usr/bin/env python3
"""
Various tools that may be useful while making plots.
"""
# WARNING: Cannot import 'rc' anywhere in this file or we get circular import
# issues. The rc param validators need functions in this file.
import functools
import re
from numbers import Integral, Real
import matplotlib.colors as mcolors
import matplotlib.font_manager as mfonts
import numpy as np
from matplotlib import rcParams as rc_matplotlib
from .externals import hsluv
from .internals import ic # noqa: F401
from .internals import _not_none, docstring, warnings
__all__ = [
'arange',
'edges',
'edges2d',
'get_colors',
'set_hue',
'set_saturation',
'set_luminance',
'set_alpha',
'shift_hue',
'scale_saturation',
'scale_luminance',
'to_hex',
'to_rgb',
'to_xyz',
'to_rgba',
'to_xyza',
'units',
'shade', # deprecated
'saturate', # deprecated
]
UNIT_REGEX = re.compile(
r'\A([-+]?[0-9._]+(?:[eE][-+]?[0-9_]+)?)(.*)\Z' # float with trailing units
)
UNIT_DICT = {
'in': 1.0,
'ft': 12.0,
'yd': 36.0,
'm': 39.37,
'dm': 3.937,
'cm': 0.3937,
'mm': 0.03937,
'pc': 1 / 6.0,
'pt': 1 / 72.0,
'ly': 3.725e17,
}
# Color docstrings
_docstring_rgba = """
color : color-spec
The color. Sanitized with `to_rgba`.
"""
_docstring_to_rgb = """
color : color-spec
The color. Can be a 3-tuple or 4-tuple of channel values, a hex
string, a registered color name, a cycle color like ``'C0'``, or
a 2-tuple colormap coordinate specification like ``('magma', 0.5)``
(see `~proplot.colors.ColorDatabase` for details).
If `space` is ``'rgb'``, this is a tuple of RGB values, and any
channels are larger than ``2``, the channels are assumed to be
on the ``0`` to ``255`` scale and are divided by ``255``.
space : {'rgb', 'hsv', 'hcl', 'hpl', 'hsl'}, optional
The colorspace for the input channel values. Ignored unless `color`
is a tuple of numbers.
cycle : str, default: :rcraw:`cycle`
The registered color cycle name used to interpret colors that
look like ``'C0'``, ``'C1'``, etc.
clip : bool, default: True
Whether to clip channel values into the valid ``0`` to ``1`` range.
Setting this to ``False`` can result in invalid colors.
"""
_docstring_space = """
space : {'hcl', 'hpl', 'hsl', 'hsv'}, optional
The hue-saturation-luminance-like colorspace used to transform the color.
Default is the strictly perceptually uniform colorspace ``'hcl'``.
"""
_docstring_hex = """
color : str
An 8-digit HEX string indicating the
red, green, blue, and alpha channel values.
"""
docstring._snippet_manager['utils.color'] = _docstring_rgba
docstring._snippet_manager['utils.hex'] = _docstring_hex
docstring._snippet_manager['utils.space'] = _docstring_space
docstring._snippet_manager['utils.to'] = _docstring_to_rgb
def _keep_units(func):
"""
Very simple decorator to strip and re-apply the same units.
"""
# NOTE: Native UnitRegistry.wraps() is not sufficient since it enforces
# unit types rather than arbitrary units. This wrapper is similar.
@functools.wraps(func)
def _with_stripped_units(data, *args, **kwargs):
units = 1
if hasattr(data, 'units') and hasattr(data, 'magnitude'):
data, units = data.magnitude, data.units
result = func(data, *args, **kwargs)
return result * units
return _with_stripped_units
def arange(min_, *args):
"""
Identical to `numpy.arange` but with inclusive endpoints. For example,
``pplt.arange(2, 4)`` returns the numpy array ``[2, 3, 4]`` instead of
``[2, 3]``. This is useful for generating lists of tick locations or
colormap levels, e.g. ``ax.format(xlocator=pplt.arange(0, 10))``
or ``ax.pcolor(levels=pplt.arange(0, 10))``.
Parameters
----------
*args : float
If three arguments are passed, these are the minimum, maximum, and step
size. If fewer than three arguments are passed, the step size is ``1``.
If one argument is passed, this is the maximum, and the minimum is ``0``.
Returns
-------
numpy.ndarray
Array of points.
See also
--------
numpy.arange
proplot.constructor.Locator
proplot.axes.CartesianAxes.format
proplot.axes.PolarAxes.format
proplot.axes.GeoAxes.format
proplot.axes.Axes.colorbar
proplot.axes.PlotAxes
"""
# Optional arguments just like np.arange
if len(args) == 0:
max_ = min_
min_ = 0
step = 1
elif len(args) == 1:
max_ = args[0]
step = 1
elif len(args) == 2:
max_ = args[0]
step = args[1]
else:
raise ValueError('Function takes from one to three arguments.')
# All input is integer
if all(isinstance(val, Integral) for val in (min_, max_, step)):
min_, max_, step = np.int64(min_), np.int64(max_), np.int64(step)
max_ += np.sign(step)
# Input is float or mixed, cast to float64
# Don't use np.nextafter with np.finfo(np.dtype(np.float64)).max, because
# round-off errors from continually adding step to min mess this up
else:
min_, max_, step = np.float64(min_), np.float64(max_), np.float64(step)
max_ += 0.5 * step
return np.arange(min_, max_, step)
@_keep_units
def edges(z, axis=-1):
"""
Calculate the approximate "edge" values along an axis given "center" values.
The size of the axis is increased by one. This is used internally to calculate
coordinate edges when you supply coordinate centers to pseudocolor commands.
Parameters
----------
z : array-like
An array of any shape.
axis : int, optional
The axis along which "edges" are calculated. The size of this
axis will be increased by one.
Returns
-------
numpy.ndarray
Array of "edge" coordinates.
See also
--------
edges2d
proplot.axes.PlotAxes.pcolor
proplot.axes.PlotAxes.pcolormesh
proplot.axes.PlotAxes.pcolorfast
"""
z = np.asarray(z)
z = np.swapaxes(z, axis, -1)
*dims, n = z.shape
zb = np.zeros((*dims, n + 1))
# Inner edges
zb[..., 1:-1] = 0.5 * (z[..., :-1] + z[..., 1:])
# Outer edges
zb[..., 0] = 1.5 * z[..., 0] - 0.5 * z[..., 1]
zb[..., -1] = 1.5 * z[..., -1] - 0.5 * z[..., -2]
return np.swapaxes(zb, axis, -1)
@_keep_units
def edges2d(z):
"""
Calculate the approximate "edge" values given a 2D grid of "center" values.
The size of both axes is increased by one. This is used internally to calculate
coordinate edges when you supply coordinate to pseudocolor commands.
Parameters
----------
z : array-like
A 2D array.
Returns
-------
numpy.ndarray
Array of "edge" coordinates.
See also
--------
edges
proplot.axes.PlotAxes.pcolor
proplot.axes.PlotAxes.pcolormesh
proplot.axes.PlotAxes.pcolorfast
"""
z = np.asarray(z)
if z.ndim != 2:
raise ValueError(f'Input must be a 2D array, but got {z.ndim}D.')
ny, nx = z.shape
zb = np.zeros((ny + 1, nx + 1))
# Inner edges
zb[1:-1, 1:-1] = 0.25 * (z[1:, 1:] + z[:-1, 1:] + z[1:, :-1] + z[:-1, :-1])
# Outer edges
zb[0, :] += edges(1.5 * z[0, :] - 0.5 * z[1, :])
zb[-1, :] += edges(1.5 * z[-1, :] - 0.5 * z[-2, :])
zb[:, 0] += edges(1.5 * z[:, 0] - 0.5 * z[:, 1])
zb[:, -1] += edges(1.5 * z[:, -1] - 0.5 * z[:, -2])
zb[[0, 0, -1, -1], [0, -1, -1, 0]] *= 0.5 # corner correction
return zb
def get_colors(*args, **kwargs):
"""
Get the colors associated with a registered or
on-the-fly color cycle or colormap.
Parameters
----------
*args, **kwargs
Passed to `~proplot.constructor.Cycle`.
Returns
-------
colors : list of str
A list of HEX strings.
See also
--------
proplot.constructor.Cycle
proplot.constructor.Colormap
"""
from .constructor import Cycle # delayed to avoid cyclic imports
cycle = Cycle(*args, **kwargs)
colors = [to_hex(dict_['color']) for dict_ in cycle]
return colors
def _transform_color(func, color, space):
"""
Standardize input for color transformation functions.
"""
*color, opacity = to_rgba(color)
color = to_xyz(color, space=space)
color = func(list(color)) # apply transform
return to_hex((*color, opacity), space=space)
@docstring._snippet_manager
def shift_hue(color, shift=0, space='hcl'):
"""
Shift the hue channel of a color.
Parameters
----------
%(utils.color)s
shift : float, optional
The HCL hue channel is offset by this value.
%(utils.space)s
Returns
-------
%(utils.hex)s
See also
--------
set_hue
set_saturation
set_luminance
set_alpha
scale_saturation
scale_luminance
"""
def func(channels):
channels[0] += shift
channels[0] %= 360
return channels
return _transform_color(func, color, space)
@docstring._snippet_manager
def scale_saturation(color, scale=1, space='hcl'):
"""
Scale the saturation channel of a color.
Parameters
----------
%(utils.color)s
scale : float, optional
The HCL saturation channel is multiplied by this value.
%(utils.space)s
Returns
-------
%(utils.hex)s
See also
--------
set_hue
set_saturation
set_luminance
set_alpha
shift_hue
scale_luminance
"""
def func(channels):
channels[1] *= scale
return channels
return _transform_color(func, color, space)
@docstring._snippet_manager
def scale_luminance(color, scale=1, space='hcl'):
"""
Scale the luminance channel of a color.
Parameters
----------
%(utils.color)s
scale : float, optional
The luminance channel is multiplied by this value.
%(utils.space)s
Returns
-------
%(utils.hex)s
See also
--------
set_hue
set_saturation
set_luminance
set_alpha
shift_hue
scale_saturation
"""
def func(channels):
channels[2] *= scale
return channels
return _transform_color(func, color, space)
@docstring._snippet_manager
def set_hue(color, hue, space='hcl'):
"""
Return a color with a different hue and the same luminance and saturation
as the input color.
Parameters
----------
%(utils.color)s
hue : float, optional
The new hue. Should lie between ``0`` and ``360`` degrees.
%(utils.space)s
Returns
-------
%(utils.hex)s
See also
--------
set_saturation
set_luminance
set_alpha
shift_hue
scale_saturation
scale_luminance
"""
def func(channels):
channels[0] = hue
return channels
return _transform_color(func, color, space)
@docstring._snippet_manager
def set_saturation(color, saturation, space='hcl'):
"""
Return a color with a different saturation and the same hue and luminance
as the input color.
Parameters
----------
%(utils.color)s
saturation : float, optional
The new saturation. Should lie between ``0`` and ``360`` degrees.
%(utils.space)s
Returns
-------
%(utils.hex)s
See also
--------
set_hue
set_luminance
set_alpha
shift_hue
scale_saturation
scale_luminance
"""
def func(channels):
channels[1] = saturation
return channels
return _transform_color(func, color, space)
@docstring._snippet_manager
def set_luminance(color, luminance, space='hcl'):
"""
Return a color with a different luminance and the same hue and saturation
as the input color.
Parameters
----------
%(utils.color)s
luminance : float, optional
The new luminance. Should lie between ``0`` and ``100``.
%(utils.space)s
Returns
-------
%(utils.hex)s
See also
--------
set_hue
set_saturation
set_alpha
shift_hue
scale_saturation
scale_luminance
"""
def func(channels):
channels[2] = luminance
return channels
return _transform_color(func, color, space)
@docstring._snippet_manager
def set_alpha(color, alpha):
"""
Return a color with the opacity channel set to the specified value.
Parameters
----------
%(utils.color)s
alpha : float, optional
The new opacity. Should be between ``0`` and ``1``.
Returns
-------
%(utils.hex)s
See also
--------
set_hue
set_saturation
set_luminance
shift_hue
scale_saturation
scale_luminance
"""
color = list(to_rgba(color))
color[3] = alpha
return to_hex(color)
def _translate_cycle_color(color, cycle=None):
"""
Parse the input cycle color.
"""
if isinstance(cycle, str):
from .colors import _cmap_database
try:
cycle = _cmap_database[cycle].colors
except (KeyError, AttributeError):
cycles = sorted(
name
for name, cmap in _cmap_database.items()
if isinstance(cmap, mcolors.ListedColormap)
)
raise ValueError(
f'Invalid color cycle {cycle!r}. Options are: '
+ ', '.join(map(repr, cycles))
+ '.'
)
elif cycle is None:
cycle = rc_matplotlib['axes.prop_cycle'].by_key()
if 'color' not in cycle:
cycle = ['k']
else:
cycle = cycle['color']
else:
raise ValueError(f'Invalid cycle {cycle!r}.')
return cycle[int(color[-1]) % len(cycle)]
@docstring._snippet_manager
def to_hex(color, space='rgb', cycle=None, keep_alpha=True):
"""
Translate the color from an arbitrary colorspace to a HEX string.
This is a generalization of `matplotlib.colors.to_hex`.
Parameters
----------
%(utils.to)s
keep_alpha : bool, default: True
Whether to keep the opacity channel. If ``True`` an 8-digit HEX
is returned. Otherwise a 6-digit HEX is returned.
Returns
-------
%(utils.hex)s
See also
--------
to_rgb
to_rgba
to_xyz
to_xyza
"""
rgba = to_rgba(color, space=space, cycle=cycle)
return mcolors.to_hex(rgba, keep_alpha=keep_alpha)
@docstring._snippet_manager
def to_rgb(color, space='rgb', cycle=None):
"""
Translate the color from an arbitrary colorspace to an RGB tuple. This is
a generalization of `matplotlib.colors.to_rgb` and the inverse of `to_xyz`.
Parameters
----------
%(utils.to)s
Returns
-------
color : 3-tuple
An RGB tuple.
See also
--------
to_hex
to_rgba
to_xyz
to_xyza
"""
return to_rgba(color, space=space, cycle=cycle)[:3]
@docstring._snippet_manager
def to_rgba(color, space='rgb', cycle=None, clip=True):
"""
Translate the color from an arbitrary colorspace to an RGBA tuple. This is
a generalization of `matplotlib.colors.to_rgba` and the inverse of `to_xyz`.
Parameters
----------
%(utils.to)s
Returns
-------
color : 4-tuple
An RGBA tuple.
See also
--------
to_hex
to_rgb
to_xyz
to_xyza
"""
# Translate color cycle strings
if isinstance(color, str) and re.match(r'\AC[0-9]\Z', color):
color = _translate_cycle_color(color, cycle=cycle)
# Translate RGB strings and (colormap, index) tuples
# NOTE: Cannot use is_color_like because might have HSL channel values
opacity = 1
if (
isinstance(color, str)
or np.iterable(color) and len(color) == 2
):
color = mcolors.to_rgba(color) # also enforced validity
if (
not np.iterable(color)
or len(color) not in (3, 4)
or not all(isinstance(c, Real) for c in color)
):
raise ValueError(f'Invalid color-spec {color!r}.')
if len(color) == 4:
*color, opacity = color
# Translate arbitrary colorspaces
if space == 'rgb':
if any(c > 2 for c in color):
color = tuple(c / 255 for c in color) # scale to within 0-1
else:
pass
elif space == 'hsv':
color = hsluv.hsl_to_rgb(*color)
elif space == 'hcl':
color = hsluv.hcl_to_rgb(*color)
elif space == 'hsl':
color = hsluv.hsluv_to_rgb(*color)
elif space == 'hpl':
color = hsluv.hpluv_to_rgb(*color)
else:
raise ValueError(f'Invalid colorspace {space!r}.')
# Clip values. This should only be disabled when testing
# translation functions.
if clip:
color = np.clip(color, 0, 1) # clip to valid range
# Return RGB or RGBA
return (*color, opacity)
@docstring._snippet_manager
def to_xyz(color, space='hcl'):
"""
Translate color in *any* format to a tuple of channel values in *any*
colorspace. This is the inverse of `to_rgb`.
Parameters
----------
%(utils.color)s
space : {'hcl', 'hpl', 'hsl', 'hsv', 'rgb'}, optional
The colorspace for the output channel values.
Returns
-------
color : 3-tuple
Tuple of channel values for the colorspace `space`.
See also
--------
to_hex
to_rgb
to_rgba
to_xyza
"""
return to_xyza(color, space)[:3]
@docstring._snippet_manager
def to_xyza(color, space='hcl'):
"""
Translate color in *any* format to a tuple of channel values in *any*
colorspace. This is the inverse of `to_rgba`.
Parameters
----------
%(utils.color)s
space : {'hcl', 'hpl', 'hsl', 'hsv', 'rgb'}, optional
The colorspace for the output channel values.
Returns
-------
color : 3-tuple
Tuple of channel values for the colorspace `space`.
See also
--------
to_hex
to_rgb
to_rgba
to_xyz
"""
# Run tuple conversions
# NOTE: Don't pass color tuple, because we may want to permit
# out-of-bounds RGB values to invert conversion
*color, opacity = to_rgba(color)
if space == 'rgb':
pass
elif space == 'hsv':
color = hsluv.rgb_to_hsl(*color) # rgb_to_hsv would also work
elif space == 'hcl':
color = hsluv.rgb_to_hcl(*color)
elif space == 'hsl':
color = hsluv.rgb_to_hsluv(*color)
elif space == 'hpl':
color = hsluv.rgb_to_hpluv(*color)
else:
raise ValueError(f'Invalid colorspace {space}.')
return (*color, opacity)
def _fontsize_to_pt(size):
"""
Translate font preset size or unit string to points.
"""
scalings = mfonts.font_scalings
if not isinstance(size, str):
return size
if size in mfonts.font_scalings:
return rc_matplotlib['font.size'] * scalings[size]
try:
return units(size, 'pt')
except ValueError:
raise KeyError(
f'Invalid font size {size!r}. Can be points or one of the preset scalings: '
+ ', '.join(f'{key!r} ({value})' for key, value in scalings.items())
+ '.'
)
@warnings._rename_kwargs('0.6.0', units='dest')
def units(
value, numeric=None, dest=None, *, fontsize=None, figure=None, axes=None, width=None
):
"""
Convert values between arbitrary physical units. This is used internally all
over proplot, permitting flexible units for various keyword arguments.
Parameters
----------
value : float or str or sequence
A size specifier or sequence of size specifiers. If numeric, units are
converted from `numeric` to `dest`. If string, units are converted to
`dest` according to the string specifier. The string should look like
``'123.456unit'``, where the number is the magnitude and ``'unit'``
matches a key in the below table.
.. _units_table:
========= =====================================================
Key Description
========= =====================================================
``'m'`` Meters
``'dm'`` Decimeters
``'cm'`` Centimeters
``'mm'`` Millimeters
``'yd'`` Yards
``'ft'`` Feet
``'in'`` Inches
``'pc'`` `Pica <pc_>`_ (1/6 inches)
``'pt'`` `Points <pt_>`_ (1/72 inches)
``'px'`` Pixels on screen, using dpi of :rcraw:`figure.dpi`
``'pp'`` Pixels once printed, using dpi of :rcraw:`savefig.dpi`
``'em'`` `Em square <em_>`_ for :rcraw:`font.size`
``'en'`` `En square <en_>`_ for :rcraw:`font.size`
``'Em'`` `Em square <em_>`_ for :rcraw:`axes.titlesize`
``'En'`` `En square <en_>`_ for :rcraw:`axes.titlesize`
``'ax'`` Axes-relative units (not always available)
``'fig'`` Figure-relative units (not always available)
``'ly'`` Light years ;)
========= =====================================================
.. _pt: https://en.wikipedia.org/wiki/Point_(typography)
.. _pc: https://en.wikipedia.org/wiki/Pica_(typography)
.. _em: https://en.wikipedia.org/wiki/Em_(typography)
.. _en: https://en.wikipedia.org/wiki/En_(typography)
numeric : str, default: 'in'
The units associated with numeric input.
dest : str, default: `numeric`
The destination units.
fontsize : str or float, default: :rc:`font.size` or :rc:`axes.titlesize`
The font size in points used for scaling. Default is
:rcraw:`font.size` for ``em`` and ``en`` units and
:rcraw:`axes.titlesize` for ``Em`` and ``En`` units.
axes : `~matplotlib.axes.Axes`, optional
The axes to use for scaling units that look like ``'0.1ax'``.
figure : `~matplotlib.figure.Figure`, optional
The figure to use for scaling units that look like ``'0.1fig'``.
If not provided we try to get the figure from ``axes.figure``.
width : bool, optional
Whether to use the width or height for the axes and figure
relative coordinates.
"""
# Scales for converting physical units to inches
fontsize_small = _not_none(fontsize, rc_matplotlib['font.size']) # always absolute
fontsize_small = _fontsize_to_pt(fontsize_small)
fontsize_large = _not_none(fontsize, rc_matplotlib['axes.titlesize'])
fontsize_large = _fontsize_to_pt(fontsize_large)
unit_dict = UNIT_DICT.copy()
unit_dict.update(
{
'em': fontsize_small / 72.0,
'en': 0.5 * fontsize_small / 72.0,
'Em': fontsize_large / 72.0,
'En': 0.5 * fontsize_large / 72.0,
}
)
# Scales for converting display units to inches
# WARNING: In ipython shell these take the value 'figure'
if not isinstance(rc_matplotlib['figure.dpi'], str):
unit_dict['px'] = 1 / rc_matplotlib['figure.dpi'] # once generated by backend
if not isinstance(rc_matplotlib['savefig.dpi'], str):
unit_dict['pp'] = 1 / rc_matplotlib['savefig.dpi'] # once 'printed' i.e. saved
# Scales relative to axes and figure objects
if axes is not None and hasattr(axes, '_get_size_inches'): # proplot axes
unit_dict['ax'] = axes._get_size_inches()[1 - int(width)]
if figure is None:
figure = getattr(axes, 'figure', None)
if figure is not None and hasattr(figure, 'get_size_inches'):
unit_dict['fig'] = figure.get_size_inches()[1 - int(width)]
# Scale for converting inches to arbitrary other unit
if numeric is None and dest is None:
numeric = dest = 'in'
elif numeric is None:
numeric = dest
elif dest is None:
dest = numeric
options = 'Valid units are ' + ', '.join(map(repr, unit_dict)) + '.'
try:
nscale = unit_dict[numeric]
except KeyError:
raise ValueError(f'Invalid numeric units {numeric!r}. ' + options)
try:
dscale = unit_dict[dest]
except KeyError:
raise ValueError(f'Invalid destination units {dest!r}. ' + options)
# Convert units for each value in list
result = []
singleton = not np.iterable(value) or isinstance(value, str)
for val in (value,) if singleton else value:
# Silently pass None
if val is None:
result.append(val)
continue
# Get unit string
if isinstance(val, Real):
number, units = val, None
elif isinstance(val, str):
regex = UNIT_REGEX.match(val)
if regex:
number, units = regex.groups() # second group is exponential
else:
raise ValueError(f'Invalid unit size spec {val!r}.')
else:
raise ValueError(f'Invalid unit size spec {val!r}.')
# Convert with units
if not units:
result.append(float(number) * nscale / dscale)
elif units in unit_dict:
result.append(float(number) * unit_dict[units] / dscale)
else:
raise ValueError(f'Invalid input units {units!r}. ' + options)
return result[0] if singleton else result
# Deprecations
shade, saturate = warnings._rename_objs(
'0.6.0',
shade=scale_luminance,
saturate=scale_saturation,
)