-
Notifications
You must be signed in to change notification settings - Fork 96
Expand file tree
/
Copy pathconstructor.py
More file actions
1565 lines (1447 loc) · 73.9 KB
/
constructor.py
File metadata and controls
1565 lines (1447 loc) · 73.9 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
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#!/usr/bin/env python3
"""
The constructor functions used to build class instances from simple shorthand arguments.
"""
# NOTE: These functions used to be in separate files like crs.py and
# ticker.py but makes more sense to group them together to ensure usage is
# consistent and so online documentation is easier to understand. Also in
# future version classes will not be imported into top-level namespace. This
# change will be easier to do with all constructor functions in separate file.
# NOTE: Used to include the raw variable names that define string keys as
# part of documentation, but this is redundant and pollutes the namespace.
# User should just inspect docstrings, use trial-error, or see online tables.
import copy
import os
import re
from functools import partial
from numbers import Number
import cycler
import matplotlib.colors as mcolors
import matplotlib.dates as mdates
import matplotlib.projections.polar as mpolar
import matplotlib.scale as mscale
import matplotlib.ticker as mticker
import numpy as np
from . import colors as pcolors
from . import proj as pproj
from . import scale as pscale
from . import ticker as pticker
from .config import rc
from .internals import ic # noqa: F401
from .internals import _not_none, _pop_props, _version_cartopy, _version_mpl, warnings
from .utils import get_colors, to_hex, to_rgba
try:
from mpl_toolkits.basemap import Basemap
except ImportError:
Basemap = object
try:
import cartopy.crs as ccrs
from cartopy.crs import Projection
except ModuleNotFoundError:
ccrs = None
Projection = object
__all__ = [
'Proj',
'Locator',
'Formatter',
'Scale',
'Colormap',
'Norm',
'Cycle',
'Colors', # deprecated
]
# Color cycle constants
# TODO: Also automatically truncate the 'bright' end of colormaps
# when building color cycles from colormaps? Or add simple option.
DEFAULT_CYCLE_SAMPLES = 10
DEFAULT_CYCLE_LUMINANCE = 90
# Normalizer registry
NORMS = {
'none': mcolors.NoNorm,
'null': mcolors.NoNorm,
'div': pcolors.DivergingNorm,
'diverging': pcolors.DivergingNorm,
'segmented': pcolors.SegmentedNorm,
'segments': pcolors.SegmentedNorm,
'log': mcolors.LogNorm,
'linear': mcolors.Normalize,
'power': mcolors.PowerNorm,
'symlog': mcolors.SymLogNorm,
}
if hasattr(mcolors, 'TwoSlopeNorm'):
NORMS['twoslope'] = mcolors.TwoSlopeNorm
# Locator registry
# NOTE: Will raise error when you try to use degree-minute-second
# locators with cartopy < 0.18.
LOCATORS = {
'none': mticker.NullLocator,
'null': mticker.NullLocator,
'auto': mticker.AutoLocator,
'log': mticker.LogLocator,
'maxn': mticker.MaxNLocator,
'linear': mticker.LinearLocator,
'multiple': mticker.MultipleLocator,
'fixed': mticker.FixedLocator,
'index': pticker.IndexLocator,
'discrete': pticker.DiscreteLocator,
'discreteminor': partial(pticker.DiscreteLocator, minor=True),
'symlog': mticker.SymmetricalLogLocator,
'logit': mticker.LogitLocator,
'minor': mticker.AutoMinorLocator,
'date': mdates.AutoDateLocator,
'microsecond': mdates.MicrosecondLocator,
'second': mdates.SecondLocator,
'minute': mdates.MinuteLocator,
'hour': mdates.HourLocator,
'day': mdates.DayLocator,
'weekday': mdates.WeekdayLocator,
'month': mdates.MonthLocator,
'year': mdates.YearLocator,
'lon': partial(pticker.LongitudeLocator, dms=False),
'lat': partial(pticker.LatitudeLocator, dms=False),
'deglon': partial(pticker.LongitudeLocator, dms=False),
'deglat': partial(pticker.LatitudeLocator, dms=False),
}
if hasattr(mpolar, 'ThetaLocator'):
LOCATORS['theta'] = mpolar.ThetaLocator
if _version_cartopy >= '0.18':
LOCATORS['dms'] = partial(pticker.DegreeLocator, dms=True)
LOCATORS['dmslon'] = partial(pticker.LongitudeLocator, dms=True)
LOCATORS['dmslat'] = partial(pticker.LatitudeLocator, dms=True)
# Formatter registry
# NOTE: Critical to use SimpleFormatter for cardinal formatters rather than
# AutoFormatter because latter fails with Basemap formatting.
# NOTE: Define cartopy longitude/latitude formatters with dms=True because that
# is their distinguishing feature relative to proplot formatter.
# NOTE: Will raise error when you try to use degree-minute-second
# formatters with cartopy < 0.18.
FORMATTERS = { # note default LogFormatter uses ugly e+00 notation
'none': mticker.NullFormatter,
'null': mticker.NullFormatter,
'auto': pticker.AutoFormatter,
'date': mdates.AutoDateFormatter,
'scalar': mticker.ScalarFormatter,
'simple': pticker.SimpleFormatter,
'fixed': mticker.FixedLocator,
'index': pticker.IndexFormatter,
'sci': pticker.SciFormatter,
'sigfig': pticker.SigFigFormatter,
'frac': pticker.FracFormatter,
'func': mticker.FuncFormatter,
'strmethod': mticker.StrMethodFormatter,
'formatstr': mticker.FormatStrFormatter,
'datestr': mdates.DateFormatter,
'log': mticker.LogFormatterSciNotation, # NOTE: this is subclass of Mathtext class
'logit': mticker.LogitFormatter,
'eng': mticker.EngFormatter,
'percent': mticker.PercentFormatter,
'e': partial(pticker.FracFormatter, symbol=r'$e$', number=np.e),
'pi': partial(pticker.FracFormatter, symbol=r'$\pi$', number=np.pi),
'tau': partial(pticker.FracFormatter, symbol=r'$\tau$', number=2 * np.pi),
'lat': partial(pticker.SimpleFormatter, negpos='SN'),
'lon': partial(pticker.SimpleFormatter, negpos='WE', wraprange=(-180, 180)),
'deg': partial(pticker.SimpleFormatter, suffix='\N{DEGREE SIGN}'),
'deglat': partial(pticker.SimpleFormatter, suffix='\N{DEGREE SIGN}', negpos='SN'),
'deglon': partial(pticker.SimpleFormatter, suffix='\N{DEGREE SIGN}', negpos='WE', wraprange=(-180, 180)), # noqa: E501
'math': mticker.LogFormatterMathtext, # deprecated (use SciNotation subclass)
}
if hasattr(mpolar, 'ThetaFormatter'):
FORMATTERS['theta'] = mpolar.ThetaFormatter
if hasattr(mdates, 'ConciseDateFormatter'):
FORMATTERS['concise'] = mdates.ConciseDateFormatter
if _version_cartopy >= '0.18':
FORMATTERS['dms'] = partial(pticker.DegreeFormatter, dms=True)
FORMATTERS['dmslon'] = partial(pticker.LongitudeFormatter, dms=True)
FORMATTERS['dmslat'] = partial(pticker.LatitudeFormatter, dms=True)
# Scale registry and presets
SCALES = mscale._scale_mapping
SCALES_PRESETS = {
'quadratic': ('power', 2,),
'cubic': ('power', 3,),
'quartic': ('power', 4,),
'height': ('exp', np.e, -1 / 7, 1013.25, True),
'pressure': ('exp', np.e, -1 / 7, 1013.25, False),
'db': ('exp', 10, 1, 0.1, True),
'idb': ('exp', 10, 1, 0.1, False),
'np': ('exp', np.e, 1, 1, True),
'inp': ('exp', np.e, 1, 1, False),
}
mscale.register_scale(pscale.CutoffScale)
mscale.register_scale(pscale.ExpScale)
mscale.register_scale(pscale.FuncScale)
mscale.register_scale(pscale.InverseScale)
mscale.register_scale(pscale.LogScale)
mscale.register_scale(pscale.LinearScale)
mscale.register_scale(pscale.LogitScale)
mscale.register_scale(pscale.MercatorLatitudeScale)
mscale.register_scale(pscale.PowerScale)
mscale.register_scale(pscale.SineLatitudeScale)
mscale.register_scale(pscale.SymmetricalLogScale)
# Cartopy projection registry and basemap default keyword args
# NOTE: Normally basemap raises error if you omit keyword args
PROJ_DEFAULTS = {
'geos': {'lon_0': 0},
'eck4': {'lon_0': 0},
'moll': {'lon_0': 0},
'hammer': {'lon_0': 0},
'kav7': {'lon_0': 0},
'sinu': {'lon_0': 0},
'vandg': {'lon_0': 0},
'mbtfpq': {'lon_0': 0},
'robin': {'lon_0': 0},
'ortho': {'lon_0': 0, 'lat_0': 0},
'nsper': {'lon_0': 0, 'lat_0': 0},
'aea': {'lon_0': 0, 'lat_0': 90, 'width': 15000e3, 'height': 15000e3},
'eqdc': {'lon_0': 0, 'lat_0': 90, 'width': 15000e3, 'height': 15000e3},
'cass': {'lon_0': 0, 'lat_0': 90, 'width': 15000e3, 'height': 15000e3},
'gnom': {'lon_0': 0, 'lat_0': 90, 'width': 15000e3, 'height': 15000e3},
'poly': {'lon_0': 0, 'lat_0': 0, 'width': 10000e3, 'height': 10000e3},
'npaeqd': {'lon_0': 0, 'boundinglat': 10}, # NOTE: everything breaks if you
'nplaea': {'lon_0': 0, 'boundinglat': 10}, # try to set boundinglat to zero
'npstere': {'lon_0': 0, 'boundinglat': 10},
'spaeqd': {'lon_0': 0, 'boundinglat': -10},
'splaea': {'lon_0': 0, 'boundinglat': -10},
'spstere': {'lon_0': 0, 'boundinglat': -10},
'lcc': {
'lon_0': 0, 'lat_0': 40, 'lat_1': 35, 'lat_2': 45, # use cartopy defaults
'width': 20000e3, 'height': 15000e3
},
'tmerc': {
'lon_0': 0, 'lat_0': 0, 'width': 10000e3, 'height': 10000e3
},
'merc': {
'llcrnrlat': -80, 'urcrnrlat': 84, 'llcrnrlon': -180, 'urcrnrlon': 180
},
'omerc': {
'lat_0': 0, 'lon_0': 0, 'lat_1': -10, 'lat_2': 10,
'lon_1': 0, 'lon_2': 0, 'width': 10000e3, 'height': 10000e3
},
}
if ccrs is None:
PROJS = {}
else:
PROJS = {
'aitoff': pproj.Aitoff,
'hammer': pproj.Hammer,
'kav7': pproj.KavrayskiyVII,
'wintri': pproj.WinkelTripel,
'npgnom': pproj.NorthPolarGnomonic,
'spgnom': pproj.SouthPolarGnomonic,
'npaeqd': pproj.NorthPolarAzimuthalEquidistant,
'spaeqd': pproj.SouthPolarAzimuthalEquidistant,
'nplaea': pproj.NorthPolarLambertAzimuthalEqualArea,
'splaea': pproj.SouthPolarLambertAzimuthalEqualArea,
}
PROJS_MISSING = {
'aea': 'AlbersEqualArea',
'aeqd': 'AzimuthalEquidistant',
'cyl': 'PlateCarree', # only basemap name not matching PROJ
'eck1': 'EckertI',
'eck2': 'EckertII',
'eck3': 'EckertIII',
'eck4': 'EckertIV',
'eck5': 'EckertV',
'eck6': 'EckertVI',
'eqc': 'PlateCarree', # actual PROJ name
'eqdc': 'EquidistantConic',
'eqearth': 'EqualEarth', # better looking Robinson; not in basemap
'euro': 'EuroPP', # Europe; not in basemap or PROJ
'geos': 'Geostationary',
'gnom': 'Gnomonic',
'igh': 'InterruptedGoodeHomolosine', # not in basemap
'laea': 'LambertAzimuthalEqualArea',
'lcc': 'LambertConformal',
'lcyl': 'LambertCylindrical', # not in basemap or PROJ
'merc': 'Mercator',
'mill': 'Miller',
'moll': 'Mollweide',
'npstere': 'NorthPolarStereo', # np/sp stuff not in PROJ
'nsper': 'NearsidePerspective',
'ortho': 'Orthographic',
'osgb': 'OSGB', # UK; not in basemap or PROJ
'osni': 'OSNI', # Ireland; not in basemap or PROJ
'pcarree': 'PlateCarree', # common alternate name
'robin': 'Robinson',
'rotpole': 'RotatedPole',
'sinu': 'Sinusoidal',
'spstere': 'SouthPolarStereo',
'stere': 'Stereographic',
'tmerc': 'TransverseMercator',
'utm': 'UTM', # not in basemap
}
for _key, _cls in tuple(PROJS_MISSING.items()):
if hasattr(ccrs, _cls):
PROJS[_key] = getattr(ccrs, _cls)
del PROJS_MISSING[_key]
if PROJS_MISSING:
warnings._warn_proplot(
'The following cartopy projection(s) are unavailable: '
+ ', '.join(map(repr, PROJS_MISSING))
+ ' . Please consider updating cartopy.'
)
PROJS_TABLE = (
'The known cartopy projection classes are:\n'
+ '\n'.join(
' ' + key + ' ' * (max(map(len, PROJS)) - len(key) + 10) + cls.__name__
for key, cls in PROJS.items()
)
)
# Geographic feature properties
FEATURES_CARTOPY = { # positional arguments passed to NaturalEarthFeature
'land': ('physical', 'land'),
'ocean': ('physical', 'ocean'),
'lakes': ('physical', 'lakes'),
'coast': ('physical', 'coastline'),
'rivers': ('physical', 'rivers_lake_centerlines'),
'borders': ('cultural', 'admin_0_boundary_lines_land'),
'innerborders': ('cultural', 'admin_1_states_provinces_lakes'),
}
FEATURES_BASEMAP = { # names of relevant basemap methods
'land': 'fillcontinents',
'coast': 'drawcoastlines',
'rivers': 'drawrivers',
'borders': 'drawcountries',
'innerborders': 'drawstates',
}
# Resolution names
# NOTE: Maximum basemap resolutions are much finer than cartopy
RESOS_CARTOPY = {
'lo': '110m',
'med': '50m',
'hi': '10m',
'x-hi': '10m', # extra high
'xx-hi': '10m', # extra extra high
}
RESOS_BASEMAP = {
'lo': 'c', # coarse
'med': 'l',
'hi': 'i', # intermediate
'x-hi': 'h',
'xx-hi': 'f', # fine
}
def _modify_colormap(cmap, *, cut, left, right, reverse, shift, alpha, samples):
"""
Modify colormap using a variety of methods.
"""
if cut is not None or left is not None or right is not None:
if isinstance(cmap, pcolors.DiscreteColormap):
if cut is not None:
warnings._warn_proplot(
"Invalid argument 'cut' for ListedColormap. Ignoring."
)
cmap = cmap.truncate(left=left, right=right)
else:
cmap = cmap.cut(cut, left=left, right=right)
if reverse:
cmap = cmap.reversed()
if shift is not None:
cmap = cmap.shifted(shift)
if alpha is not None:
cmap = cmap.copy(alpha=alpha)
if samples is not None:
if isinstance(cmap, pcolors.DiscreteColormap):
cmap = cmap.copy(N=samples)
else:
cmap = cmap.to_discrete(samples)
return cmap
@warnings._rename_kwargs(
'0.8.0', fade='saturation', shade='luminance', to_listed='discrete'
)
def Colormap(
*args, name=None, listmode='perceptual', filemode='continuous', discrete=False,
cycle=None, save=False, save_kw=None, **kwargs
):
"""
Generate, retrieve, modify, and/or merge instances of
`~proplot.colors.PerceptualColormap`,
`~proplot.colors.ContinuousColormap`, and
`~proplot.colors.DiscreteColormap`.
Parameters
----------
*args : colormap-spec
Positional arguments that individually generate colormaps. If more
than one argument is passed, the resulting colormaps are *merged* with
`~proplot.colors.ContinuousColormap.append`
or `~proplot.colors.DiscreteColormap.append`.
The arguments are interpreted as follows:
* If a registered colormap name, that colormap instance is looked up.
If colormap instance is a native matplotlib colormap class, it is
converted to a proplot colormap class.
* If a filename string with valid extension, the colormap data
is loaded with `proplot.colors.ContinuousColormap.from_file` or
`proplot.colors.DiscreteColormap.from_file` depending on the value of
`filemode` (see below). Default behavior is to load a
`~proplot.colors.ContinuousColormap`.
* If RGB tuple or color string, a `~proplot.colors.PerceptualColormap`
is generated with `~proplot.colors.PerceptualColormap.from_color`.
If the string ends in ``'_r'``, the monochromatic map will be
*reversed*, i.e. will go from dark to light instead of light to dark.
* If sequence of RGB tuples or color strings, a
`~proplot.colors.DiscreteColormap`, `~proplot.colors.PerceptualColormap`,
or `~proplot.colors.ContinuousColormap` is generated depending on
the value of `listmode` (see below). Default behavior is to generate a
`~proplot.colors.PerceptualColormap`.
* If dictionary, a `~proplot.colors.PerceptualColormap` is
generated with `~proplot.colors.PerceptualColormap.from_hsl`.
The dictionary should contain the keys ``'hue'``, ``'saturation'``,
``'luminance'``, and optionally ``'alpha'``, or their aliases (see below).
name : str, optional
Name under which the final colormap is registered. It can
then be reused by passing ``cmap='name'`` to plotting
functions. Names with leading underscores are ignored.
filemode : {'perceptual', 'continuous', 'discrete'}, optional
Controls how colormaps are generated when you input list(s) of colors.
The options are as follows:
* If ``'perceptual'`` or ``'continuous'``, a colormap is generated using
`~proplot.colors.ContinuousColormap.from_file`. The resulting
colormap may be a `~proplot.colors.ContinuousColormap` or
`~proplot.colors.PerceptualColormap` depending on the data file.
* If ``'discrete'``, a `~proplot.colors.DiscreteColormap` is generated
using `~proplot.colors.ContinuousColormap.from_file`.
Default is ``'continuous'`` when calling `Colormap` directly and
``'discrete'`` when `Colormap` is called by `Cycle`.
listmode : {'perceptual', 'continuous', 'discrete'}, optional
Controls how colormaps are generated when you input sequence(s)
of colors. The options are as follows:
* If ``'perceptual'``, a `~proplot.colors.PerceptualColormap`
is generated with `~proplot.colors.PerceptualColormap.from_list`.
* If ``'continuous'``, a `~proplot.colors.ContinuousColormap` is
generated with `~proplot.colors.ContinuousColormap.from_list`.
* If ``'discrete'``, a `~proplot.colors.DiscreteColormap` is generated
by simply passing the colors to the class.
Default is ``'perceptual'`` when calling `Colormap` directly and
``'discrete'`` when `Colormap` is called by `Cycle`.
samples : int or sequence of int, optional
For `~proplot.colors.ContinuousColormap`\\ s, this is used to
generate `~proplot.colors.DiscreteColormap`\\ s with
`~proplot.colors.ContinuousColormap.to_discrete`. For
`~proplot.colors.DiscreteColormap`\\ s, this is used to updates the
number of colors in the cycle. If `samples` is integer, it applies
to the final *merged* colormap. If it is a sequence of integers,
it applies to each input colormap individually.
discrete : bool, optional
If ``True``, when the final colormap is a
`~proplot.colors.DiscreteColormap`, we leave it alone, but when it is a
`~proplot.colors.ContinuousColormap`, we always call
`~proplot.colors.ContinuousColormap.to_discrete` with a
default `samples` value of ``10``. This argument is not
necessary if you provide the `samples` argument.
left, right : float or sequence of float, optional
Truncate the left or right edges of the colormap.
Passed to `~proplot.colors.ContinuousColormap.truncate`.
If float, these apply to the final *merged* colormap. If sequence
of float, these apply to each input colormap individually.
cut : float or sequence of float, optional
Cut out the center of the colormap. Passed to
`~proplot.colors.ContinuousColormap.cut`. If float,
this applies to the final *merged* colormap. If sequence of
float, these apply to each input colormap individually.
reverse : bool or sequence of bool, optional
Reverse the colormap. Passed to
`~proplot.colors.ContinuousColormap.reversed`. If
float, this applies to the final *merged* colormap. If
sequence of float, these apply to each input colormap individually.
shift : float or sequence of float, optional
Cyclically shift the colormap.
Passed to `~proplot.colors.ContinuousColormap.shifted`.
If float, this applies to the final *merged* colormap. If sequence
of float, these apply to each input colormap individually.
a
Shorthand for `alpha`.
alpha : float or color-spec or sequence, optional
The opacity of the colormap or the opacity gradation. Passed to
`proplot.colors.ContinuousColormap.set_alpha`
or `proplot.colors.DiscreteColormap.set_alpha`. If float, this applies
to the final *merged* colormap. If sequence of float, these apply to
each colormap individually.
h, s, l, c
Shorthands for `hue`, `luminance`, `saturation`, and `chroma`.
hue, saturation, luminance : float or color-spec or sequence, optional
The channel value(s) used to generate colormaps with
`~proplot.colors.PerceptualColormap.from_hsl` and
`~proplot.colors.PerceptualColormap.from_color`.
* If you provided no positional arguments, these are used to create
an arbitrary perceptually uniform colormap with
`~proplot.colors.PerceptualColormap.from_hsl`. This
is an alternative to passing a dictionary as a positional argument
with `hue`, `saturation`, and `luminance` as dictionary keys (see `args`).
* If you did provide positional arguments, and any of them are
color specifications, these control the look of monochromatic colormaps
generated with `~proplot.colors.PerceptualColormap.from_color`.
To use different values for each colormap, pass a sequence of floats
instead of a single float. Note the default `luminance` is ``90`` if
`discrete` is ``True`` and ``100`` otherwise.
chroma
Alias for `saturation`.
cycle : str, optional
The registered cycle name used to interpret color strings like ``'C0'``
and ``'C2'``. Default is from the active property :rcraw:`cycle`. This lets
you make monochromatic colormaps using colors selected from arbitrary cycles.
save : bool, optional
Whether to call the colormap/color cycle save method, i.e.
`proplot.colors.ContinuousColormap.save` or
`proplot.colors.DiscreteColormap.save`.
save_kw : dict-like, optional
Ignored if `save` is ``False``. Passed to the colormap/color cycle
save method, i.e. `proplot.colors.ContinuousColormap.save` or
`proplot.colors.DiscreteColormap.save`.
Other parameters
----------------
**kwargs
Passed to `proplot.colors.ContinuousColormap.copy`,
`proplot.colors.PerceptualColormap.copy`, or
`proplot.colors.DiscreteColormap.copy`.
Returns
-------
matplotlib.colors.Colormap
A `~proplot.colors.ContinuousColormap` or
`~proplot.colors.DiscreteColormap` instance.
See also
--------
matplotlib.colors.Colormap
matplotlib.colors.LinearSegmentedColormap
matplotlib.colors.ListedColormap
proplot.constructor.Norm
proplot.constructor.Cycle
proplot.utils.get_colors
"""
# Helper function
# NOTE: Very careful here! Try to support common use cases. For example
# adding opacity gradations to colormaps with Colormap('cmap', alpha=(0.5, 1))
# or sampling maps with Colormap('cmap', samples=np.linspace(0, 1, 11)) should
# be allowable.
# If *args is singleton try to preserve it.
def _pop_modification(key):
value = kwargs.pop(key, None)
if not np.iterable(value) or isinstance(value, str):
values = (None,) * len(args)
elif len(args) == len(value):
values, value = tuple(value), None
elif len(args) == 1: # e.g. Colormap('cmap', alpha=(0.5, 1))
values = (None,)
else:
raise ValueError(
f'Got {len(args)} colormap-specs '
f'but {len(value)} values for {key!r}.'
)
return value, values
# Parse keyword args that can apply to the merged colormap or each one
hsla = _pop_props(kwargs, 'hsla')
if not args and hsla.keys() - {'alpha'}:
args = (hsla,)
else:
kwargs.update(hsla)
default_luminance = kwargs.pop('default_luminance', None) # used internally
cut, cuts = _pop_modification('cut')
left, lefts = _pop_modification('left')
right, rights = _pop_modification('right')
shift, shifts = _pop_modification('shift')
reverse, reverses = _pop_modification('reverse')
samples, sampless = _pop_modification('samples')
alpha, alphas = _pop_modification('alpha')
luminance, luminances = _pop_modification('luminance')
saturation, saturations = _pop_modification('saturation')
if luminance is not None:
luminances = (luminance,) * len(args)
if saturation is not None:
saturations = (saturation,) * len(args)
# Issue warnings and errors
if not args:
raise ValueError(
'Colormap() requires either positional arguments or '
"'hue', 'chroma', 'saturation', and/or 'luminance' keywords."
)
deprecated = {'listed': 'discrete', 'linear': 'continuous'}
if listmode in deprecated:
oldmode, listmode = listmode, deprecated[listmode]
warnings._warn_proplot(
f'Please use listmode={listmode!r} instead of listmode={oldmode!r}.'
'Option was renamed in v0.8 and will be removed in a future relase.'
)
options = {'discrete', 'continuous', 'perceptual'}
for key, mode in zip(('listmode', 'filemode'), (listmode, filemode)):
if mode not in options:
raise ValueError(
f'Invalid {key}={mode!r}. Options are: '
+ ', '.join(map(repr, options))
+ '.'
)
# Loop through colormaps
cmaps = []
for arg, icut, ileft, iright, ireverse, ishift, isamples, iluminance, isaturation, ialpha in zip( # noqa: E501
args, cuts, lefts, rights, reverses, shifts, sampless, luminances, saturations, alphas # noqa: E501
):
# Load registered colormaps and maps on file
# TODO: Document how 'listmode' also affects loaded files
if isinstance(arg, str):
if '.' in arg and os.path.isfile(arg):
if filemode == 'discrete':
arg = pcolors.DiscreteColormap.from_file(arg)
else:
arg = pcolors.ContinuousColormap.from_file(arg)
else:
try:
arg = pcolors._cmap_database[arg]
except KeyError:
pass
# Convert matplotlib colormaps to subclasses
if isinstance(arg, mcolors.Colormap):
cmap = pcolors._translate_cmap(arg)
# Dictionary of hue/sat/luminance values or 2-tuples
elif isinstance(arg, dict):
cmap = pcolors.PerceptualColormap.from_hsl(**arg)
# List of color tuples or color strings, i.e. iterable of iterables
elif (
not isinstance(arg, str) and np.iterable(arg)
and all(np.iterable(color) for color in arg)
):
if listmode == 'discrete':
cmap = pcolors.DiscreteColormap(arg)
elif listmode == 'continuous':
cmap = pcolors.ContinuousColormap.from_list(arg)
else:
cmap = pcolors.PerceptualColormap.from_list(arg)
# Monochrome colormap from input color
# NOTE: Do not print color names in error message. Too long to be useful.
else:
jreverse = isinstance(arg, str) and arg[-2:] == '_r'
if jreverse:
arg = arg[:-2]
try:
color = to_rgba(arg, cycle=cycle)
except (ValueError, TypeError):
message = f'Invalid colormap, color cycle, or color {arg!r}.'
if isinstance(arg, str) and arg[:1] != '#':
message += (
' Options include: '
+ ', '.join(sorted(map(repr, pcolors._cmap_database)))
+ '.'
)
raise ValueError(message) from None
iluminance = _not_none(iluminance, default_luminance)
cmap = pcolors.PerceptualColormap.from_color(
color, luminance=iluminance, saturation=isaturation
)
ireverse = _not_none(ireverse, False)
ireverse = ireverse ^ jreverse # xor
# Modify the colormap
cmap = _modify_colormap(
cmap, cut=icut, left=ileft, right=iright,
reverse=ireverse, shift=ishift, alpha=ialpha, samples=isamples,
)
cmaps.append(cmap)
# Merge the resulting colormaps
if len(cmaps) > 1: # more than one map and modify arbitrary properties
cmap = cmaps[0].append(*cmaps[1:], **kwargs)
else:
cmap = cmaps[0].copy(**kwargs)
# Modify the colormap
if discrete and isinstance(cmap, pcolors.ContinuousColormap): # noqa: E501
samples = _not_none(samples, DEFAULT_CYCLE_SAMPLES)
cmap = _modify_colormap(
cmap, cut=cut, left=left, right=right,
reverse=reverse, shift=shift, alpha=alpha, samples=samples
)
# Initialize
if not cmap._isinit:
cmap._init()
# Register the colormap
if name is None:
name = cmap.name # may have been modified by e.g. reversed()
else:
cmap.name = name
if not isinstance(name, str):
raise ValueError('The colormap name must be a string.')
pcolors._cmap_database[name] = cmap
# Save the colormap
if save:
save_kw = save_kw or {}
cmap.save(**save_kw)
return cmap
def Cycle(*args, N=None, samples=None, name=None, **kwargs):
"""
Generate and merge `~cycler.Cycler` instances in a variety of ways.
Parameters
----------
*args : colormap-spec or cycle-spec, optional
Positional arguments control the *colors* in the `~cycler.Cycler`
object. If zero arguments are passed, the single color ``'black'``
is used. If more than one argument is passed, the resulting cycles
are merged. Arguments are interpreted as follows:
* If a `~cycler.Cycler`, nothing more is done.
* If a sequence of RGB tuples or color strings, these colors are used.
* If a `~proplot.colors.DiscreteColormap`, colors from the ``colors``
attribute are used.
* If a string cycle name, that `~proplot.colors.DiscreteColormap`
is looked up and its ``colors`` are used.
* In all other cases, the argument is passed to `Colormap`, and
colors from the resulting `~proplot.colors.ContinuousColormap`
are used. See the `samples` argument.
If the last positional argument is numeric, it is used for the
`samples` keyword argument.
N
Shorthand for `samples`.
samples : float or sequence of float, optional
For `~proplot.colors.DiscreteColormap`\\ s, this is the number of
colors to select. For example, ``Cycle('538', 4)`` returns the first 4
colors of the ``'538'`` color cycle.
For `~proplot.colors.ContinuousColormap`\\ s, this is either a
sequence of sample coordinates used to draw colors from the colormap, or
an integer number of colors to draw. If the latter, the sample coordinates
are ``np.linspace(0, 1, samples)``. For example, ``Cycle('Reds', 5)``
divides the ``'Reds'`` colormap into five evenly spaced colors.
Other parameters
----------------
c, color, colors : sequence of color-spec, optional
A sequence of colors passed as keyword arguments. This is equivalent
to passing a sequence of colors as the first positional argument and is
included for consistency with `~matplotlib.axes.Axes.set_prop_cycle`.
If positional arguments were passed, the colors in this list are
appended to the colors resulting from the positional arguments.
lw, ls, d, a, m, ms, mew, mec, mfc
Shorthands for the below keywords.
linewidth, linestyle, dashes, alpha, marker, markersize, markeredgewidth, \
markeredgecolor, markerfacecolor : object or sequence of object, optional
Lists of `~matplotlib.lines.Line2D` properties that can be added to the
`~cycler.Cycler` instance. If the input was already a `~cycler.Cycler`,
these are added or appended to the existing cycle keys. If the lists have
unequal length, they are repeated to their least common multiple (unlike
`~cycler.cycler`, which throws an error in this case). For more info
on cyclers see `~matplotlib.axes.Axes.set_prop_cycle`. Also see
the `line style reference \
<https://matplotlib.org/2.2.5/gallery/lines_bars_and_markers/line_styles_reference.html>`__,
the `marker reference \
<https://matplotlib.org/stable/gallery/lines_bars_and_markers/marker_reference.html>`__,
and the `custom dashes reference \
<https://matplotlib.org/stable/gallery/lines_bars_and_markers/line_demo_dash_control.html>`__.
linewidths, linestyles, dashes, alphas, markers, markersizes, markeredgewidths, \
markeredgecolors, markerfacecolors
Aliases for the above keywords.
**kwargs
If the input is not already a `~cycler.Cycler` instance, these are passed
to `Colormap` and used to build the `~proplot.colors.DiscreteColormap`
from which the cycler will draw its colors.
Returns
-------
cycler.Cycler
A `~cycler.Cycler` instance that can be passed
to `~matplotlib.axes.Axes.set_prop_cycle`.
See also
--------
cycler.cycler
cycler.Cycler
matplotlib.axes.Axes.set_prop_cycle
proplot.constructor.Colormap
proplot.constructor.Norm
proplot.utils.get_colors
"""
# Parse keyword arguments that rotate through other properties
# besides color cycles.
props = _pop_props(kwargs, 'line')
if 'sizes' in kwargs: # special case, gets translated back by scatter()
props.setdefault('markersize', kwargs.pop('sizes'))
samples = _not_none(samples=samples, N=N) # trigger Colormap default
for key, value in tuple(props.items()): # permit in-place modification
if value is None:
return
elif not np.iterable(value) or isinstance(value, str):
value = (value,)
props[key] = list(value) # ensure mutable list
# If args is non-empty, means we want color cycle; otherwise is black
if not args:
props.setdefault('color', ['black'])
if kwargs:
warnings._warn_proplot(f'Ignoring Cycle() keyword arg(s) {kwargs}.')
dicts = ()
# Merge cycler objects and/or update cycler objects with input kwargs
elif all(isinstance(arg, cycler.Cycler) for arg in args):
if kwargs:
warnings._warn_proplot(f'Ignoring Cycle() keyword arg(s) {kwargs}.')
if len(args) == 1 and not props:
return args[0]
dicts = tuple(arg.by_key() for arg in args)
# Get a cycler from a colormap
# NOTE: Passing discrete=True does not imply default_luminance=90 because
# someone might be trying to make qualitative colormap for use in 2D plot
else:
if isinstance(args[-1], Number):
args, samples = args[:-1], _not_none(samples_positional=args[-1], samples=samples) # noqa: #501
kwargs.setdefault('listmode', 'discrete')
kwargs.setdefault('filemode', 'discrete')
kwargs['discrete'] = True # triggers application of default 'samples'
kwargs['default_luminance'] = DEFAULT_CYCLE_LUMINANCE
cmap = Colormap(*args, name=name, samples=samples, **kwargs)
name = _not_none(name, cmap.name)
dict_ = {'color': [c if isinstance(c, str) else to_hex(c) for c in cmap.colors]}
dicts = (dict_,)
# Update the cyler property
dicts = dicts + (props,)
props = {}
for dict_ in dicts:
for key, value in dict_.items():
props.setdefault(key, []).extend(value)
# Build cycler with matching property lengths
maxlen = np.lcm.reduce([len(value) for value in props.values()])
props = {key: value * (maxlen // len(value)) for key, value in props.items()}
cycle = cycler.cycler(**props)
cycle.name = _not_none(name, '_no_name')
return cycle
def Norm(norm, *args, **kwargs):
"""
Return an arbitrary `~matplotlib.colors.Normalize` instance. See this
`tutorial <https://matplotlib.org/stable/tutorials/colors/colormapnorms.html>`__
for an introduction to matplotlib normalizers.
Parameters
----------
norm : str or `~matplotlib.colors.Normalize`
The normalizer specification. If a `~matplotlib.colors.Normalize`
instance already, a `copy.copy` of the instance is returned.
Otherwise, `norm` should be a string corresponding to one of
the "registered" colormap normalizers (see below table).
If `norm` is a list or tuple and the first element is a "registered"
normalizer name, subsequent elements are passed to the normalizer class
as positional arguments.
.. _norm_table:
=============================== =====================================
Key(s) Class
=============================== =====================================
``'null'``, ``'none'`` `~matplotlib.colors.NoNorm`
``'diverging'``, ``'div'`` `~proplot.colors.DivergingNorm`
``'segmented'``, ``'segments'`` `~proplot.colors.SegmentedNorm`
``'linear'`` `~matplotlib.colors.Normalize`
``'log'`` `~matplotlib.colors.LogNorm`
``'power'`` `~matplotlib.colors.PowerNorm`
``'symlog'`` `~matplotlib.colors.SymLogNorm`
=============================== =====================================
Other parameters
----------------
*args, **kwargs
Passed to the `~matplotlib.colors.Normalize` initializer.
Returns
-------
matplotlib.colors.Normalize
A `~matplotlib.colors.Normalize` instance.
See also
--------
matplotlib.colors.Normalize
proplot.colors.DiscreteNorm
proplot.constructor.Colormap
"""
if np.iterable(norm) and not isinstance(norm, str):
norm, *args = *norm, *args
if isinstance(norm, mcolors.Normalize):
return copy.copy(norm)
if not isinstance(norm, str):
raise ValueError(f'Invalid norm name {norm!r}. Must be string.')
if norm not in NORMS:
raise ValueError(
f'Unknown normalizer {norm!r}. Options are: '
+ ', '.join(map(repr, NORMS))
+ '.'
)
if norm == 'symlog' and not args and 'linthresh' not in kwargs:
kwargs['linthresh'] = 1 # special case, needs argument
return NORMS[norm](*args, **kwargs)
def Locator(locator, *args, discrete=False, **kwargs):
"""
Return a `~matplotlib.ticker.Locator` instance.
Parameters
----------
locator : `~matplotlib.ticker.Locator`, str, bool, float, or sequence
The locator specification, interpreted as follows:
* If a `~matplotlib.ticker.Locator` instance already,
a `copy.copy` of the instance is returned.
* If ``False``, a `~matplotlib.ticker.NullLocator` is used, and if
``True``, the default `~matplotlib.ticker.AutoLocator` is used.
* If a number, this specifies the *step size* between tick locations.
Returns a `~matplotlib.ticker.MultipleLocator`.
* If a sequence of numbers, these points are ticked. Returns
a `~matplotlib.ticker.FixedLocator` by default or a
`~proplot.ticker.DiscreteLocator` if `discrete` is ``True``.
Otherwise, `locator` should be a string corresponding to one
of the "registered" locators (see below table). If `locator` is a
list or tuple and the first element is a "registered" locator name,
subsequent elements are passed to the locator class as positional
arguments. For example, ``pplt.Locator(('multiple', 5))`` is
equivalent to ``pplt.Locator('multiple', 5)``.
.. _locator_table:
======================= ============================================ =====================================================================================
Key Class Description
======================= ============================================ =====================================================================================
``'null'``, ``'none'`` `~matplotlib.ticker.NullLocator` No ticks
``'auto'`` `~matplotlib.ticker.AutoLocator` Major ticks at sensible locations
``'minor'`` `~matplotlib.ticker.AutoMinorLocator` Minor ticks at sensible locations
``'date'`` `~matplotlib.dates.AutoDateLocator` Default tick locations for datetime axes
``'fixed'`` `~matplotlib.ticker.FixedLocator` Ticks at these exact locations
``'discrete'`` `~proplot.ticker.DiscreteLocator` Major ticks restricted to these locations but subsampled depending on the axis length
``'discreteminor'`` `~proplot.ticker.DiscreteLocator` Minor ticks restricted to these locations but subsampled depending on the axis length
``'index'`` `~proplot.ticker.IndexLocator` Ticks on the non-negative integers
``'linear'`` `~matplotlib.ticker.LinearLocator` Exactly ``N`` ticks encompassing axis limits, spaced as ``numpy.linspace(lo, hi, N)``
``'log'`` `~matplotlib.ticker.LogLocator` For log-scale axes
``'logminor'`` `~matplotlib.ticker.LogLocator` For log-scale axes on the 1st through 9th multiples of each power of the base
``'logit'`` `~matplotlib.ticker.LogitLocator` For logit-scale axes
``'logitminor'`` `~matplotlib.ticker.LogitLocator` For logit-scale axes with ``minor=True`` passed to `~matplotlib.ticker.LogitLocator`
``'maxn'`` `~matplotlib.ticker.MaxNLocator` No more than ``N`` ticks at sensible locations
``'multiple'`` `~matplotlib.ticker.MultipleLocator` Ticks every ``N`` step away from zero
``'symlog'`` `~matplotlib.ticker.SymmetricalLogLocator` For symlog-scale axes
``'symlogminor'`` `~matplotlib.ticker.SymmetricalLogLocator` For symlog-scale axes on the 1st through 9th multiples of each power of the base
``'theta'`` `~matplotlib.projections.polar.ThetaLocator` Like the base locator but default locations are every `numpy.pi` / 8 radians
``'year'`` `~matplotlib.dates.YearLocator` Ticks every ``N`` years
``'month'`` `~matplotlib.dates.MonthLocator` Ticks every ``N`` months
``'weekday'`` `~matplotlib.dates.WeekdayLocator` Ticks every ``N`` weekdays
``'day'`` `~matplotlib.dates.DayLocator` Ticks every ``N`` days
``'hour'`` `~matplotlib.dates.HourLocator` Ticks every ``N`` hours
``'minute'`` `~matplotlib.dates.MinuteLocator` Ticks every ``N`` minutes
``'second'`` `~matplotlib.dates.SecondLocator` Ticks every ``N`` seconds
``'microsecond'`` `~matplotlib.dates.MicrosecondLocator` Ticks every ``N`` microseconds
``'lon'``, ``'deglon'`` `~proplot.ticker.LongitudeLocator` Longitude gridlines at sensible decimal locations
``'lat'``, ``'deglat'`` `~proplot.ticker.LatitudeLocator` Latitude gridlines at sensible decimal locations
``'dms'`` `~proplot.ticker.DegreeLocator` Gridlines on nice minute and second intervals
``'dmslon'`` `~proplot.ticker.LongitudeLocator` Longitude gridlines on nice minute and second intervals
``'dmslat'`` `~proplot.ticker.LatitudeLocator` Latitude gridlines on nice minute and second intervals
======================= ============================================ =====================================================================================
Other parameters
----------------
*args, **kwargs
Passed to the `~matplotlib.ticker.Locator` class.
Returns
-------
matplotlib.ticker.Locator
A `~matplotlib.ticker.Locator` instance.
See also
--------
matplotlib.ticker.Locator
proplot.axes.CartesianAxes.format
proplot.axes.PolarAxes.format
proplot.axes.GeoAxes.format
proplot.axes.Axes.colorbar
proplot.constructor.Formatter
""" # noqa: E501
if np.iterable(locator) and not isinstance(locator, str) and not all(
isinstance(num, Number) for num in locator
):
locator, *args = *locator, *args
if isinstance(locator, mticker.Locator):
return copy.copy(locator)