/
axes.py
2579 lines (2440 loc) 路 124 KB
/
axes.py
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
"""
This page documents the axes subclasses that can be returned by
`~proplot.subplots.subplots` and their various method wrappers. You should
start with the documentation on the following methods:
* `BaseAxes.format`
* `CartesianAxes.format_partial`
* `ProjectionAxes.format_partial`
* `BaseAxes.format_partial`
`BaseAxes.format` calls the various ``format_partial`` methods in turn,
and is your **one-stop-shop for changing axes settings** like
*x* and *y* axis limits, axis labels, tick locations, tick labels
grid lines, axis scales, titles, a-b-c labelling, adding
geographic features, and much more.
.. raw:: html
<h1>Developer notes</h1>
Axes method wrappers are documented in the "functions" table. The wrappers are
dynamically applied within the `~proplot.axes.BaseAxes.__getattribute__` methods
on `~proplot.axes.BaseAxes` and its subclasses. But why doesn't ProPlot just
use *decorators*? Two reasons.
1. Brevity. For example: `~proplot.wrappers.cmap_wrapper` overrides **a dozen** different
methods. This lets me override these methods in *one* line, instead of 50
lines. To see which methods are overriden, the user can simply check the
documentation.
2. Documentation. If I wrapped every method, the sphinx `autodoc <http://www.sphinx-doc.org/en/master/usage/extensions/autodoc.html>`_
documentation generator would inherit docstrings from the parent methods.
In other words, the plotting method docstrings would get duplicated on
my website from the matplotlib website, generally with a bunch of errors.
I could also override these methods with my own docstrings, but that would
mean when the user tries e.g. ``help(ax.contourf)``, they would see my
own, brief docstring instead of the comprehensive matplotlib reference
they were probably looking for! I only add a handful of features to these
functions, but the native methods generally have way more options.
It should be noted that dynamically wrapping every time the user requests a
method is slower than "decoration", which just wraps the method when the class
is declared. But this was not found to significantly affect performance. And
anyway, `Premature Optimization is the Root of All Evil
<http://wiki.c2.com/?PrematureOptimization>`__.
"""
# WARNING: Wanted to bulk wrap methods using __new__ on a *metaclass*, since
# this would just wrap method *once* and not every single time user accesses
# object. More elegant, but __new__ *does not receive inherited methods* (that
# comes later down the line), so we can't wrap them. Anyway overriding
# __getattribute__ is fine, and premature optimiztaion is root of all evil!
import numpy as np
import warnings
from numbers import Number
import matplotlib.projections as mproj
import matplotlib.axes as maxes
import matplotlib.dates as mdates
import matplotlib.text as mtext
import matplotlib.ticker as mticker
import matplotlib.patches as mpatches
import matplotlib.gridspec as mgridspec
import matplotlib.transforms as mtransforms
import matplotlib.collections as mcollections
from .rctools import rc, _rc_names_nodots
from . import utils, projs, axistools, wrappers
from .utils import _default, units
__all__ = [
'BaseAxes',
'BasemapProjectionAxes',
'CartesianAxes',
'CartopyProjectionAxes',
'EmptyPanel', 'PanelAxes',
'PolarAxes', 'ProjectionAxes',
]
# Aliases for panel names
_panel_aliases = {
'bpanel': 'bottompanel',
'rpanel': 'rightpanel',
'tpanel': 'toppanel',
'lpanel': 'leftpanel',
'bcolorbar': 'bottompanel',
'rcolorbar': 'rightpanel',
'tcolorbar': 'toppanel',
'lcolorbar': 'leftpanel',
'blegend': 'bottompanel',
'rlegend': 'rightpanel',
'tlegend': 'toppanel',
'llegend': 'leftpanel',
'bottomcolorbar': 'bottompanel',
'rightcolorbar': 'rightpanel',
'topcolorbar': 'toppanel',
'leftcolorbar': 'leftpanel',
'bottomlegend': 'bottompanel',
'rightlegend': 'rightpanel',
'toplegend': 'toppanel',
'leftlegend': 'leftpanel'
}
# Helper function
_abc_string = 'abcdefghijklmnopqrstuvwxyz'
def _abc(i):
"""Function for a-b-c labeling, returns a...z...aa...zz...aaa...zzz."""
if i < 26:
return _abc_string[i]
else:
return _abc(i - 26) + _abc_string[i % 26] # sexy sexy recursion
# Import mapping toolbox
try:
from cartopy.mpl.geoaxes import GeoAxes
except ModuleNotFoundError:
GeoAxes = object
#-----------------------------------------------------------------------------#
# Generalized custom axes class
#-----------------------------------------------------------------------------#
def _redraw_text(obj, overwrite=True, **kwargs):
"""Allows updating new text properties introduced by override."""
# Attempt update, but will raise error if e.g. border is passed
if overwrite:
try:
obj.update(kwargs)
return obj
except Exception:
pass
obj.set_visible(False) # destroy original text instance
# Get properties
text = kwargs.pop('text', obj.get_text())
for key in ('color', 'weight', 'fontsize'):
kwargs[key] = getattr(obj, 'get_' + key)()
# Position
pos = obj.get_position()
x, y = kwargs.pop('position', (None,None))
x = _default(kwargs.pop('x', x), pos[0])
y = _default(kwargs.pop('y', y), pos[1])
# Return new object
return obj.axes.text(x, y, text, **kwargs)
class BaseAxes(maxes.Axes):
"""Lowest-level axes subclass. Handles titles and axis
sharing. Adds several new methods and overrides existing ones."""
name = 'base'
"""The registered projection name."""
def __init__(self, *args, number=None,
sharex=None, sharey=None, spanx=None, spany=None,
sharex_level=0, sharey_level=0,
**kwargs):
"""
Parameters
----------
number : int
The subplot number, used for a-b-c labelling (see
`~BaseAxes.format`).
sharex_level, sharey_level : {3, 2, 1, 0}, optional
The "axis sharing level" for the *x* axis, *y* axis, or both
axes. See `~proplot.subplots.subplots` for details.
sharex, sharey : `BaseAxes`, optional
Axes to use for *x* and *y* axis sharing.
spanx, spany : bool, optional
Boolean toggle for whether spanning labels are enabled for the
*x* axis, *y* axis, or both axes. See `~proplot.subplots.subplots`
for details.
See also
--------
`CartesianAxes`, `ProjectionAxes`
"""
# Properties
# TODO: Just use subspec get_columns_and_rows, instead of saving
# these properties! Redundant! See align_xlabels Figure for example.
self.number = number # for abc numbering
self._yrange = None # geometry, filled later
self._xrange = None
self._nrows = None
self._ncols = None
# Misc necessary
self._xrotated = False # whether manual rotation was applied
self._titles_dict = {} # dictionar of title text objects and their locations
self._gridliner_on = False # whether cartopy gridliners are enabled
self._aspect_equal = None # for imshow and stuff
self._is_map = False # needed by wrappers, which can't import this file
# Children and related properties
self.bottompanel = EmptyPanel()
self.toppanel = EmptyPanel()
self.leftpanel = EmptyPanel()
self.rightpanel = EmptyPanel()
self._tight_bbox = None # save these
self._zoom = None # the 'zoom lines' for inset zoom-in axes
self._panel_parent = None
self._inset_zoom = False
self._inset_parent = None # filled later
self._inset_children = [] # arbitrary number of insets possible
self._colorbar_parent = None
self._colorbar_child = None # the *actual* axes, with content and whatnot; may be needed for tight subplot stuff
self._auto_colorbar = {} # stores plot handles for filling with a colorbar, as function of location
self._auto_legend = {} # as above, but for legend
self._auto_colorbar_kw = {} # keyword args for automatic colorbar() call
self._auto_legend_kw = {} # as above, but for automatic legend() call
self._filled = False # turned off when panels filled with colorbar or legend
self._alty_child = None
self._altx_child = None
self._alty_parent = None
self._altx_parent = None
self._dualy_scale = None # for scaling units on opposite side of ax, and holding data limits fixed
self._dualx_scale = None
self._panels_main_gridspec = None # filled with gridspec used for axes subplot and its panels
self._panels_stack_gridspec = None # filled with gridspec used for 'stacked' panels
self._xtick_pad_error = (0,0)
self._ytick_pad_error = (0,0)
# Call parent
super().__init__(*args, **kwargs)
# Axis sharing, title stuff, new text attributes
self._spanx = spanx # boolean toggles, whether we want to span axes labels
self._spany = spany
self._sharex_setup(sharex, sharex_level)
self._sharey_setup(sharey, sharey_level)
self._title_transform = self.title.get_transform() # save in case it changes
width, height = self.figure.get_size_inches()
self.width = abs(self._position.width)*width # position is in figure units
self.height = abs(self._position.height)*height
coltransform = mtransforms.blended_transform_factory(self.transAxes, self.figure.transFigure)
rowtransform = mtransforms.blended_transform_factory(self.figure.transFigure, self.transAxes)
self.abc = self.text(0, 0, '') # position tbd
self.collabel = self.text(0.5, 0, '', va='bottom', ha='center', transform=coltransform)
self.rowlabel = self.text(0, 0.5, '', va='center', ha='right', transform=rowtransform)
# Apply custom props
# Make sure tick length is zero for polar plots, or azimuthal labels
# are excessively offset from the border.
kw = {}
if isinstance(self, mproj.PolarAxes):
kw.setdefault('ticklen', 0)
self.format(mode=1, **kw)
@wrappers._expand_methods_list
def __getattribute__(self, attr, *args):
"""Applies the `~proplot.wrappers.text_wrapper` wrapper and disables
the redundant methods `_disabled_methods`. Enables the attribute aliases
``bpanel`` for ``bottompanel``, ``tpanel`` for ``toppanel``,
``lpanel`` for ``leftpanel``, and ``rpanel`` for ``rightpanel``."""
attr = _panel_aliases.get(attr, attr)
obj = super().__getattribute__(attr, *args)
# Disabled methods
for message,attrs in wrappers._disabled_methods.items():
if attr in attrs:
raise RuntimeError(message.format(attr))
# Non-plotting overrides
# All plotting overrides are implemented in individual subclasses
if attr=='text':
obj = wrappers._text_wrapper(self, obj)
return obj
def _share_short_axis(self, share, side, level):
"""When sharing main subplots, shares the short axes of their side panels."""
if isinstance(self, PanelAxes):
return
axis = 'x' if side[0] in 'lr' else 'y'
paxs1 = getattr(self, side + 'panel') # calling this means, share properties on this axes with input 'share' axes
paxs2 = getattr(share, side + 'panel')
if not all(pax and pax.get_visible() and not pax._filled for pax in paxs1) or \
not all(pax and pax.get_visible() and not pax._filled for pax in paxs2):
return
if len(paxs1) != len(paxs2):
raise RuntimeError('Sync error. Different number of stacked panels along axes on like column/row of figure.')
for pax1,pax2 in zip(paxs1,paxs2):
getattr(pax1, '_share' + axis + '_setup')(pax2, level)
def _share_long_axis(self, share, side, level):
"""When sharing main subplots, shares the long axes of their side panels,
assuming long axis sharing is enabled for that panel."""
if isinstance(self, PanelAxes):
return
axis = 'x' if side[0] in 'tb' else 'y'
paxs = getattr(self, side + 'panel') # calling this means, share properties on this axes with input 'share' axes
if not all(pax and pax.get_visible() and not pax._filled for pax in paxs) or \
not all(pax._share for pax in paxs):
return
for pax in paxs:
getattr(pax, '_share' + axis + '_setup')(share, level)
def _sharex_setup(self, sharex, level):
"""Sets up shared axes. The input is the 'parent' axes, from which
this one will draw its properties."""
if sharex is None or sharex is self:
return
if isinstance(self, ProjectionAxes) or isinstance(sharex, ProjectionAxes):
return
if level not in range(4):
raise ValueError('Level can be 1 (do not share limits, just hide axis labels), 2 (share limits, but do not hide tick labels), or 3 (share limits and hide tick labels).')
# Account for side panels
self._share_short_axis(sharex, 'left', level)
self._share_short_axis(sharex, 'right', level)
self._share_long_axis(sharex, 'bottom', level)
self._share_long_axis(sharex, 'top', level)
# Builtin features
self._sharex = sharex
if level>1:
self._shared_x_axes.join(self, sharex)
# Make axis and tick labels invisible for "shared" axes
# TODO: Why does this work?! Is only called on initialization, but
# shouldn't this be overridden when user changes e.g. the formatter,
# since the tick label objects themselves will also change? Maybe
# new tick labels inherit properties from old tick labels.
self.xaxis.label.set_visible(False)
if level>2:
for t in self.xaxis.get_ticklabels():
t.set_visible(False)
def _sharey_setup(self, sharey, level):
"""Sets up shared axes. The input is the 'parent' axes, from which
this one will draw its properties."""
if sharey is None or sharey is self:
return
if isinstance(self, ProjectionAxes) or isinstance(sharey, ProjectionAxes):
return
if level not in range(4):
raise ValueError('Level can be 1 (do not share limits, just hide axis labels), 2 (share limits, but do not hide tick labels), or 3 (share limits and hide tick labels).')
# Account for side panels
self._share_short_axis(sharey, 'bottom', level)
self._share_short_axis(sharey, 'top', level)
self._share_long_axis(sharey, 'left', level)
self._share_long_axis(sharey, 'right', level)
# Builtin features
self._sharey = sharey
if level>1:
self._shared_y_axes.join(self, sharey)
# "Shared" axis and tick labels
if level>2:
for t in self.yaxis.get_ticklabels():
t.set_visible(False)
self.yaxis.label.set_visible(False)
def _title_kwargs(self, abc=False, loc=None):
"""Position title text to the left, center, or right and either
inside or outside the axes. The default is center, outside."""
# Apply rc settings
prefix = 'abc' if abc else 'title'
kwargs = rc.fill({
'fontsize': f'{prefix}.fontsize',
'weight': f'{prefix}.weight',
'color': f'{prefix}.color',
'fontfamily': 'font.family'
})
if loc is None:
loc = rc[f'{prefix}.loc']
if loc is None:
return kwargs
# Add border props if we are moving it
kwargs.update(rc.fill({
'border': f'{prefix}.border',
'linewidth': f'{prefix}.linewidth',
}, cache=False)) # look up defaults
# Get coordinates
ypad = rc.get('axes.titlepad')/(72*self.height) # to inches --> to axes relative
xpad = rc.get('axes.titlepad')/(72*self.width) # why not use the same for x?
if isinstance(loc, str): # coordinates
# Get horizontal position
if loc in ('c','uc','lc','center','upper center','lower center'):
x, ha = 0.5, 'center'
elif loc in ('l','ul','ll','left','upper left','lower left'):
x, ha = 1.5*xpad*(loc not in ('l','left')), 'left'
elif loc in ('r','ur','lr','right','upper right','lower right'):
x, ha = 1 - 1.5*xpad*(loc not in ('r','right')), 'right'
else:
raise ValueError(f'Invalid "loc" {loc}.')
# Get vertical position
transform = self.transAxes
if loc in ('c','l','r','center','left','right'):
y, va = 1, 'bottom' # leave it alone, may be adjusted during draw-time to account for axis label (fails to adjust for tick labels; see notebook)
transform, kwargs['border'] = self._title_transform, False
elif loc in ('ul','ur','uc','upper left','upper right','upper center'):
y, va = 1 - 1.5*ypad, 'top'
else:
y, va = 1.5*ypad, 'bottom'
elif np.iterable(loc) and len(loc)==2:
ha = va = 'center'
x, y = loc
transform = self.transAxes
else:
raise ValueError(f'Invalid "loc" {loc}.')
# Return kwargs
kwargs.update({'x':x, 'y':y, 'ha':ha, 'va':va, 'transform':transform})
return kwargs
def format(self, *, mode=2, rc_kw=None, **kwargs):
"""
Sets up temporary rc settings and calls `CartesianAxes.format_partial`
or `ProjectionAxes.format_partial`.
Parameters
----------
rc_kw : dict, optional
A dictionary containing "rc" configuration settings that will
be applied to this axes. Temporarily updates the
`~proplot.rctools.rc` object. See `~proplot.rctools` for details.
**kwargs
Any of three options:
* A keyword arg for `BaseAxes.format_partial`,
`CartesianAxes.format_partial`, or `ProjectionAxes.format_partial`.
* A global "rc" keyword arg, like ``linewidth`` or ``color``.
* A standard "rc" keyword arg **with the dots omitted**.
For example, ``land.color`` becomes ``landcolor``.
The latter two options update the `~proplot.rctools.rc`
object, just like `rc_kw`.
Other parameters
----------------
mode : int, optional
The "getitem mode". This is used under-the-hood -- you shouldn't
have to use it directly. Determines whether queries to the
`~proplot.rctools.rc` object will ignore `rcParams <https://matplotlib.org/users/customizing.html>`__.
This can help prevent a massive number of unnecessary lookups
when the settings haven't been changed by the user.
See `~proplot.rctools.rc_configurator` for details.
"""
# Figure out which kwargs are valid rc settings
# TODO: Support for 'small', 'large', etc. font
kw = {} # for format
rc_kw = rc_kw or {}
for key,value in kwargs.items():
key_fixed = _rc_names_nodots.get(key, None)
if key_fixed is None:
kw[key] = value
else:
rc_kw[key_fixed] = value
rc._getitem_mode = 0 # might still be non-zero if had error
# Apply special defaults on first format call for flush panel axes
if mode==1 and isinstance(self, PanelAxes) and self._flush:
axis = ('y' if self._side in ('right','left') else 'x')
for key in ('labelloc','ticklabelloc'):
kw.setdefault(axis + key, self._side) # e.g. xlabelloc and xticklabelloc set to bottom for flush bottom panels
# Call format in context of custom settings
with rc.context(rc_kw, mode=mode):
self.format_partial(**kw)
def format_partial(self, title=None, top=True,
figtitle=None, suptitle=None, collabels=None, rowlabels=None, # label rows and columns
**kwargs, # nopanel optionally puts title and abc label in main axes
):
"""
Called by `CartesianAxes.format_partial` and `ProjectionAxes.format_partial`,
formats the axes titles, a-b-c labelling, row and column labels, and
figure title.
Note that the `abc`, `abcformat`, `abcloc`, and `titleloc` keyword
arguments are actually rc configuration settings that are temporarily
changed by the call to `~BaseAxes.format`. They are documented here
because it is extremely common to change them with `~BaseAxes.format`.
They also appear in the tables in the `~proplot.rctools` documention.
Parameters
----------
title : str, optional
The axes title.
ltitle, rtitle, ultitle, uctitle, urtitle, lltitle, lctitle, lrtitle : str, optional
Axes titles, with the first part of the name indicating location.
This lets you specify multiple "title" within a single axes. See
the `titleloc` keyword.
abc : bool, optional
Whether to apply "a-b-c" subplot labelling based on the
``number`` attribute. If ``number`` is >26, the labels will loop
around to a, ..., z, aa, ..., zz, aaa, ..., zzz, ... God help you
if you ever need that many labels. Defaults to ``rc['abc']``.
abcformat : str, optional
It is a string containing the character ``a`` or ``A``, specifying
the format of a-b-c labels. ``'a'`` is the default, but e.g.
``'a.'``, ``'a)'``, or ``'A'`` might be desirable. Defaults to
``rc['abc.format']``.
abcloc, titleloc : str, optional
They are strings indicating the location for the a-b-c label and
main title. The following locations keys are valid. Defaults to
``rc['abc.loc']`` and ``rc['title.loc']``.
========================= ============================
Location Valid keys
========================= ============================
center, above axes ``'center'``, ``'c'``
left, above axes ``'left'``, ``'l'``
right, above axes ``'right'``, ``'r'``
lower center, inside axes ``'lower center``', ``'lc'``
upper center, inside axes ``'upper center'``, ``'uc'``
upper right, inside axes ``'upper right'``, ``'ur'``
upper left, inside axes ``'upper left'``, ``'ul'``
lower left, inside axes ``'lower left'``, ``'ll'``
lower right, inside axes ``'lower right'``, ``'lr'``
========================= ============================
abcborder, titleborder : bool, optional
These are the ``rc['abc.border']`` and ``rc['title.border']``
settings. They indicate whether to draw a border around the
labels, which can help them stand out on top of artists plotted
inside the axes.
top : bool, optional
Whether to try to put title and a-b-c label above the top subplot
panel (if it exists), or to always put them on the main subplot.
Defaults to ``True``, i.e. the former.
rowlabels, colllabels : list of str, optional
The subplot row and column labels. If list, length must match
the number of subplot rows, columns.
figtitle, suptitle : str, optional
The figure "super" title, centered between the left edge of
the lefmost column of subplots and the right edge of the rightmost
column of subplots, and automatically offset above figure titles.
This is more sophisticated than matplotlib's builtin "super title",
which is just centered between the figure edges and offset from
the top edge.
"""
# Figure patch (for some reason needs to be re-asserted even if
# declared before figure is drawn)
# Look into `~matplotlib.axes.SubplotBase.is_last_row` and
# `~matplotlib.axes.SubplotBase.is_first_column` methods.
kw = rc.fill({'facecolor':'figure.facecolor'})
self.figure.patch.update(kw)
# Super title and labels
# NOTE: These are actually *figure-wide* settings, but that line seems
# to get blurred -- where we have shared axes, spanning labels, and
# whatnot. May result in redundant assignments if formatting more than
# one axes, but operations are fast so some redundancy is nbd.
fig = self.figure # the figure
suptitle = figtitle or suptitle
kw = rc.fill({
'fontsize': 'suptitle.fontsize',
'weight': 'suptitle.weight',
'color': 'suptitle.color',
'fontfamily': 'font.family'
})
if suptitle or kw: # if either is not None or non-empty
fig._setup_suptitle(suptitle, **kw)
kw = rc.fill({
'fontsize': 'rowlabel.fontsize',
'weight': 'rowlabel.weight',
'color': 'rowlabel.color',
'fontfamily': 'font.family'
})
if rowlabels or kw:
fig._setup_labels(self, rowlabels, rows=True, **kw)
kw = rc.fill({
'fontsize': 'collabel.fontsize',
'weight': 'collabel.weight',
'color': 'collabel.color',
'fontfamily': 'font.family'
})
if collabels or kw:
fig._setup_labels(self, collabels, rows=False, **kw)
# Axes for title or abc
# NOTE: We check filled property but top panel filled is not allowed, change this?
pax = self.toppanel[0]
if top and pax and pax.get_visible() and not pax._filled:
tax = self.toppanel[0]
else:
tax = self
tax = tax._altx_child or tax # always on top!
# Create axes title
# NOTE: Aligning title flush against left or right of axes is alredy a
# matplotlib feature: set_title(loc={'center','left','right'}). This
# version just has more features and flexibility.
kw = tax._title_kwargs(abc=False)
if title is not None:
kw['text'] = title
if kw:
tax.title = _redraw_text(tax.title, **kw)
titles_dict = {}
for key,title in tax._titles_dict.items():
titles_dict[key] = _redraw_text(title, **kw)
tax._titles_dict = titles_dict
# Alternate titles
for key,title in kwargs.items():
if not key[-5:]=='title':
raise ValueError(f'format() got an unexpected keyword argument "{key}".')
loc = key[:-5]
kw = tax._title_kwargs(abc=False, loc=loc)
obj = tax._titles_dict.get(loc, tax.title)
tax._titles_dict[loc] = _redraw_text(obj, text=title, overwrite=(obj is not tax.title), **kw)
# Initial text setup
# Will only occur if user requests change, or on initial run
abc = rc['abc']
abcformat = rc['abc.format']
if abcformat and self.number is not None:
if 'a' not in abcformat and 'A' not in abcformat:
raise ValueError(f'Invalid abcformat "{abcformat}". Must include letter "a" or "A".')
abcedges = abcformat.split('a' if 'a' in abcformat else 'A')
text = abcedges[0] + _abc(self.number-1) + abcedges[-1]
if 'A' in abcformat:
text = text.upper()
tax.abc.set_text(text)
# Apply any changed or new settings
kw = tax._title_kwargs(abc=True)
if kw:
tax.abc = _redraw_text(tax.abc, **kw)
if abc is not None: # set invisible initially
tax.abc.set_visible(bool(abc))
def legend(self, *args, loc=None, **kwargs):
"""
Adds an *inset* legend, or calls the `PanelAxes.legend` method
for the panel location at `loc`. See `~matplotlib.axes.Axes.legend`
and `~proplot.wrappers.legend_wrapper` for details.
Parameters
----------
loc : int or str, optional
The legend location or panel location. The following location keys
are valid. Note that if a panel location is specified, the panel
must already exist, i.e. it was generated by your call to
`~proplot.subplots.subplots`!
================== ==========================================================
Location Valid keys
================== ==========================================================
"best" possible ``0``, ``'best'``, ``'b'``, ``'i'``, ``'inset'``
upper right ``1``, ``'upper right'``, ``'ur'``
upper left ``2``, ``'upper left'``, ``'ul'``
lower left ``3``, ``'lower left'``, ``'ll'``
lower right ``4``, ``'lower right'``, ``'lr'``
center left ``5``, ``'center left'``, ``'cl'``
center right ``6``, ``'center right'``, ``'cr'``
lower center ``7``, ``'lower center'``, ``'lc'``
upper center ``8``, ``'upper center'``, ``'uc'``
center ``9``, ``'center'``, ``'c'``
left panel ``'l'``, ``'left'``
right panel ``'r'``, ``'right'``
bottom panel ``'b'``, ``'bottom'``
top panel ``'t'``, ``'top'``
================== ==========================================================
*args, **kwargs
Passed to `~matplotlib.axes.Axes.legend`.
"""
ax, loc = wrappers._get_panel(self, loc)
if loc=='fill':
return ax.legend(*args, **kwargs)
return wrappers.legend_wrapper(ax, *args, loc=loc, **kwargs)
def colorbar(self, *args, loc=None, pad=None,
length=None, width=None, xspace=None, frame=None, frameon=None,
alpha=None, linewidth=None, edgecolor=None, facecolor=None,
**kwargs):
"""
Adds an *inset* colorbar, or calls the `PanelAxes.colorbar` method for
the panel at location `loc`. See `~proplot.wrappers.colorbar_wrapper`
for details.
Parameters
----------
loc : str, optional
The colorbar location or panel location. The following location keys
are valid. Note that if a panel location is specified, the panel
must already exist, i.e. it was generated by your call to
`~proplot.subplots.subplots`!
================== ==========================================================
Location Valid keys
================== ==========================================================
upper right ``1``, ``'upper right'``, ``'ur'``
upper left ``2``, ``'upper left'``, ``'ul'``
lower left ``3``, ``'lower left'``, ``'ll'``
lower right ``4``, ``'lower right'``, ``'lr'``
left panel ``'l'``, ``'left'``
right panel ``'r'``, ``'right'``
bottom panel ``'b'``, ``'bottom'``
top panel ``'t'``, ``'top'``
================== ==========================================================
pad : str or float, optional
Space between the axes edge and the colorbar.
If float, units are inches. If string, units are interpreted by
`~proplot.utils.units`. Defaults to ``rc['colorbar.pad']``.
length : str or float, optional
The colorbar length. If float, units are inches. If string,
units are interpreted by `~proplot.utils.units`. Defaults to
``rc['colorbar.length']``.
width : str or float, optional
The colorbar width. If float, units are inches. If string,
units are interpreted by `~proplot.utils.units`. Defaults to
``rc['colorbar.width']``.
xspace : str or float, optional
Space allocated for the bottom x-label of the colorbar.
If float, units are inches. If string, units are interpreted
by `~proplot.utils.units`. Defaults to ``rc['colorbar.xspace']``.
frame, frameon : bool, optional
Whether to draw a frame behind the inset colorbar, just like
`~matplotlib.axes.Axes.legend`. Defaults to ``rc['colorbar.frameon']``.
alpha, linewidth, edgecolor, facecolor : optional
Transparency, edge width, edge color, and face color for the frame.
Defaults to ``rc['colorbar.framealpha']``, ``rc['axes.linewidth']``,
``rc['axes.edgecolor']``, and ``rc['axes.facecolor']``.
**kwargs
Passed to `~proplot.wrappers.colorbar_wrapper`.
"""
# Location
ax, loc = wrappers._get_panel(self, loc)
if loc=='fill':
return ax.colorbar(*args, **kwargs)
# Default props
loc = _default(loc, rc['colorbar.loc'])
extend = units(_default(kwargs.get('extendsize',None), rc['colorbar.extendinset']))
length = units(_default(length, rc['colorbar.length']))/self.width
width = units(_default(width, rc['colorbar.width']))/self.height
pad = units(_default(pad, rc['colorbar.axespad']))
xpad = pad/self.width
ypad = pad/self.height
# Space for labels
if kwargs.get('label', ''):
xspace = 2.4*rc['font.size']/72 + rc['xtick.major.size']/72
else:
xspace = 1.2*rc['font.size']/72 + rc['xtick.major.size']/72
xspace /= self.height
# Get location in axes-relative coordinates
# Bounds are x0, y0, width, height in axes-relative coordinate to start
if loc in ('upper right','ur'):
bounds = (1-xpad-length, 1-ypad-width)
fbounds = (1-2*xpad-length, 1-2*ypad-width-xspace)
elif loc in ('upper left','ul'):
bounds = (xpad, 1-ypad-width)
fbounds = (0, 1-2*ypad-width-xspace)
elif loc in ('lower left','ll'):
bounds = (xpad, ypad+xspace)
fbounds = (0, 0)
elif loc in ('lower right','lr','b','best'):
bounds = (1-xpad-length, ypad+xspace)
fbounds = (1-2*xpad-length, 0)
else:
raise ValueError(f'Invalid location {loc}.')
bounds = (bounds[0], bounds[1], length, width)
fbounds = (fbounds[0], fbounds[1], 2*xpad+length, 2*ypad+width+xspace)
# Make axes
locator = self._make_inset_locator(bounds, self.transAxes)
bbox = locator(None, None)
ax = maxes.Axes(self.figure, bbox.bounds, zorder=5)
ax.set_axes_locator(locator)
self.add_child_axes(ax)
# Make colorbar
# WARNING: Inset colorbars are tiny! So use smart default locator
kwargs.update({
'ticklocation':'bottom', 'edgecolor':edgecolor,
'linewidth':linewidth, 'extendsize':extend,
})
kwargs.setdefault('maxn', 5)
cb = wrappers.colorbar_wrapper(ax, *args, **kwargs)
# Make frame
# NOTE: We do not allow shadow effects or fancy edges effect.
# Also keep zorder same as with legend.
frameon = _default(frame, frameon, rc['colorbar.frameon'])
if frameon:
# Make object
xmin, ymin, width, height = fbounds
patch = mpatches.Rectangle((xmin,ymin), width, height,
snap=True, zorder=4.5, transform=self.transAxes) # fontsize defined in if statement
# Properties
alpha = _default(alpha, rc['colorbar.framealpha'])
linewidth = _default(linewidth, rc['axes.linewidth'])
edgecolor = _default(edgecolor, rc['axes.edgecolor'])
facecolor = _default(facecolor, rc['axes.facecolor'])
patch.update({'alpha':alpha, 'linewidth':linewidth, 'edgecolor':edgecolor, 'facecolor':facecolor})
self.add_artist(patch)
return cb
def inset_axes(self, *args, **kwargs):
"""Alias for `~BaseAxes.inset`."""
return self.inset(*args, **kwargs)
def inset(self, bounds, *, transform=None, zorder=5, zoom=True, zoom_kw={}, **kwargs):
"""
Like the builtin `~matplotlib.axes.Axes.inset_axes` method, but
draws an inset `CartesianAxes` axes and adds some options.
Parameters
----------
bounds : list of float
The bounds for the inset axes, listed as ``(x, y, width, height)``.
transform : {'data', 'axes', 'figure'} or `~matplotlib.transforms.Transform`, optional
The transform used to interpret `bounds`. Can be a
`~matplotlib.transforms.Transform` object or a string representing the
`~matplotlib.axes.Axes.transData`, `~matplotlib.axes.Axes.transAxes`,
or `~matplotlib.figure.Figure.transFigure` transforms. Defaults to
``'axes'``, i.e. `bounds` is in axes-relative coordinates.
zorder : float, optional
The zorder of the axes, should be greater than the zorder of
elements in the parent axes. Defaults to ``5``.
zoom : bool, optional
Whether to draw lines indicating the inset zoom using
`~BaseAxes.indicate_inset_zoom`. The lines will automatically
adjust whenever the parent axes or inset axes limits are changed.
Defaults to ``True``.
zoom_kw : dict, optional
Passed to `~BaseAxes.indicate_inset_zoom`.
Other parameters
----------------
**kwargs
Passed to `CartesianAxes`.
"""
# Carbon copy with my custom axes
if not transform:
transform = self.transAxes
else:
transform = wrappers._get_transform(self, transform)
label = kwargs.pop('label', 'inset_axes')
# This puts the rectangle into figure-relative coordinates.
locator = self._make_inset_locator(bounds, transform)
bb = locator(None, None)
ax = CartesianAxes(self.figure, bb.bounds, zorder=zorder, label=label, **kwargs)
# The following locator lets the axes move if we used data coordinates,
# is called by ax.apply_aspect()
ax.set_axes_locator(locator)
self.add_child_axes(ax)
self._inset_children.append(ax)
ax._inset_zoom = zoom
ax._inset_parent = self
# Finally add zoom (NOTE: Requires version >=3.0)
if zoom:
ax.indicate_inset_zoom(**zoom_kw)
return ax
def indicate_inset_zoom(self, alpha=None, linewidth=None, color=None, edgecolor=None, **kwargs):
"""
Called automatically when using `~BaseAxes.inset` with ``zoom=True``.
Like `~matplotlib.axes.Axes.indicate_inset_zoom`, but *refreshes* the
lines whenever `xlim` or `ylim` is passed to `BaseAxes.format` for either
the parent *or* the inset axes. This method is called from the *inset*
axes, not the parent axes.
Parameters
----------
alpha : float, optional
The transparency of the zoom box fill.
linewidth : float, optional
The width of the zoom lines and box outline in points.
color : color-spec, optional
The color of the zoom box fill.
edgecolor : color-spec, optional
The color of the zoom lines and box outline.
**kwargs
Passed to `~matplotlib.axes.Axes.indicate_inset`.
"""
# Should be called from the inset axes
parent = self._inset_parent
alpha = alpha or 1.0
linewidth = linewidth or rc['axes.linewidth']
edgecolor = color or edgecolor or rc['axes.edgecolor']
if not parent:
raise ValueError(f'{self} is not an inset axes.')
xlim = self.get_xlim()
ylim = self.get_ylim()
rect = (xlim[0], ylim[0], xlim[1] - xlim[0], ylim[1] - ylim[0])
# Call inset
kwargs.update({'linewidth':linewidth, 'edgecolor':edgecolor, 'alpha':alpha})
rectpatch, connects = parent.indicate_inset(rect, self, **kwargs)
# Adopt properties from old one
if self._zoom:
rectpatch_old, connects_old = self._zoom
rectpatch.update_from(rectpatch_old)
rectpatch_old.set_visible(False)
for line,line_old in zip(connects,connects_old):
# Actually want to *preserve* whether line is visible! This
# is automatically determined!
visible = line.get_visible()
line.update_from(line_old)
line.set_visible(visible)
line_old.set_visible(False)
# By default linewidth is only applied to box
else:
for line in connects:
line.set_linewidth(linewidth)
line.set_color(edgecolor)
line.set_alpha(alpha)
self._zoom = (rectpatch, connects)
return (rectpatch, connects)
def _make_inset_locator(self, bounds, trans):
"""Helper function, copied from private matplotlib version."""
def inset_locator(ax, renderer):
bbox = mtransforms.Bbox.from_bounds(*bounds)
bb = mtransforms.TransformedBbox(bbox, trans)
tr = self.figure.transFigure.inverted()
bb = mtransforms.TransformedBbox(bb, tr)
return bb
return inset_locator
def area(self, *args, **kwargs):
"""Alias for `~matplotlib.axes.Axes.fill_between`, which is wrapped by
`~proplot.wrappers.fill_between_wrapper`."""
return self.fill_between(*args, **kwargs)
def areax(self, *args, **kwargs):
"""Alias for `~matplotlib.axes.Axes.fill_betweenx`, which is wrapped by
`~proplot.wrappers.fill_betweenx_wrapper`."""
return self.fill_betweenx(*args, **kwargs)
def heatmap(self, *args, **kwargs):
"""Calls `~matplotlib.axes.Axes.pcolormesh` and applies default formatting
that is suitable for heatmaps: no gridlines, no minor ticks, and major
ticks at the center of each grid box."""
obj = self.pcolormesh(*args, **kwargs)
xlocator, ylocator = None, None
if hasattr(obj, '_coordinates'): # be careful in case private API changes! but this is only way to infer coordinates
xy = obj._coordinates
xy = (xy[1:,...] + xy[:-1,...])/2
xy = (xy[:,1:,:] + xy[:,:-1,:])/2
xlocator, ylocator = xy[0,:,0], xy[:,0,1]
self.format(
xgrid=False, ygrid=False, xtickminor=False, ytickminor=False,
xlocator=xlocator, ylocator=ylocator,
)
return obj
def cmapline(self, *args, values=None,
cmap=None, norm=None,
interp=0, **kwargs):
"""
Invoked by `~proplot.wrappers.plot_wrapper` when you pass the `cmap`
keyword argument to `~matplotlib.axes.Axes.plot`. Draws a "colormap line",
i.e. a line whose color changes as a function of some parametric coordinate
`values`. This is actually a collection of lines, added as a
`~matplotlib.collections.LineCollection` instance. See `this matplotlib example
<https://matplotlib.org/gallery/lines_bars_and_markers/multicolored_line.html>`__.
Parameters
----------
*args : (y,) or (x,y)
The coordinates. If `x` is not provided, it is inferred from `y`.
cmap : colormap spec, optional
The colormap specifier, passed to `~proplot.styletools.Colormap`.
values : list of float
The parametric values used to map points on the line to colors
in the colormap.
norm : normalizer spec, optional
The normalizer, passed to `~proplot.styletools.Norm`.
interp : int, optional
Number of values between each line joint and each *halfway* point
between line joints to which you want to interpolate.
"""
# First error check
# WARNING: So far this only works for 1D *x* and *y* coordinates. Cannot
# draw multiple colormap lines at once, unlike `~matplotlib.axes.Axes.plot`.
if values is None:
raise ValueError('Requires a "values" keyword arg.')
if len(args) not in (1,2):
raise ValueError(f'Requires 1-2 arguments, got {len(args)}.')
y = np.array(args[-1]).squeeze()
x = np.arange(y.shape[-1]) if len(args)==1 else np.array(args[0]).squeeze()
values = np.array(values).squeeze()
if x.ndim!=1 or y.ndim!=1 or values.ndim!=1:
raise ValueError(f'x ({x.ndim}-d), y ({y.ndim}-d), and values ({values.ndim}-d) must be 1-dimensional.')
if len(x)!=len(y) or len(x)!=len(values) or len(y)!=len(values):
raise ValueError(f'{len(x)} xs, {len(y)} ys, but {len(values)} colormap values.')
# Next draw the line
# Interpolate values to optionally allow for smooth gradations between
# values (bins=False) or color switchover halfway between points (bins=True)
# Next optionally interpolate the corresponding colormap values
# NOTE: We linearly interpolate here, but user might use a normalizer that
# e.g. performs log before selecting linear color range; don't need to
# implement that here
if interp>0:
xorig, yorig, vorig = x, y, values
x, y, values = [], [], []
for j in range(xorig.shape[0]-1):
idx = (slice(None, -1) if j+1<xorig.shape[0]-1 else slice(None))
x.extend(np.linspace(xorig[j], xorig[j+1], interp + 2)[idx].flat)
y.extend(np.linspace(yorig[j], yorig[j+1], interp + 2)[idx].flat)
values.extend(np.linspace(vorig[j], vorig[j+1], interp + 2)[idx].flat)
x, y, values = np.array(x), np.array(y), np.array(values)
coords = []
levels = utils.edges(values)
for j in range(y.shape[0]):
# Get x/y coordinates and values for points to the 'left' and
# 'right' of each joint. Also prevent duplicates.
if j==0:
xleft, yleft = [], []
else:
xleft = [(x[j-1] + x[j])/2, x[j]]
yleft = [(y[j-1] + y[j])/2, y[j]]
if j+1==y.shape[0]:
xright, yright = [], []
else:
xleft = xleft[:-1] # prevent repetition when joined with xright/yright
yleft = yleft[:-1] # actually need numbers of x/y coordinates to be same for each segment
xright = [x[j], (x[j+1] + x[j])/2]
yright = [y[j], (y[j+1] + y[j])/2]
pleft = np.stack((xleft, yleft), axis=1)
pright = np.stack((xright, yright), axis=1)
coords.append(np.concatenate((pleft, pright), axis=0))