/
base.py
2105 lines (1942 loc) 路 81.2 KB
/
base.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
"""
The base axes class used for all ProPlot figures.
"""
import copy
import re
from numbers import Integral, Number
import matplotlib.axes as maxes
import matplotlib.collections as mcollections
import matplotlib.legend as mlegend
import matplotlib.patches as mpatches
import matplotlib.projections as mprojections
import matplotlib.transforms as mtransforms
import numpy as np
from .. import constructor
from .. import gridspec as pgridspec
from ..config import rc
from ..internals import ic # noqa: F401
from ..internals import _not_none, docstring, rcsetup, warnings
from ..utils import edges, units
from . import plot as wrap
__all__ = ['Axes']
ABC_STRING = 'abcdefghijklmnopqrstuvwxyz'
KEYS_INNER = (
'border', 'borderwidth', 'bbox', 'bboxpad', 'bboxcolor', 'bboxstyle', 'bboxalpha',
)
LOC_TRANSLATE = { # for inset colorbars and legends TODO: also as text locations
'inset': 'best',
'i': 'best',
0: 'best',
1: 'upper right',
2: 'upper left',
3: 'lower left',
4: 'lower right',
5: 'center left',
6: 'center right',
7: 'lower center',
8: 'upper center',
9: 'center',
'l': 'left',
'r': 'right',
'b': 'bottom',
't': 'top',
'c': 'center',
'ur': 'upper right',
'ul': 'upper left',
'll': 'lower left',
'lr': 'lower right',
'cr': 'center right',
'cl': 'center left',
'uc': 'upper center',
'lc': 'lower center',
}
docstring.snippets['axes.other'] = """
rc_kw : dict, optional
Dictionary containing `~proplot.config.rc` settings applied to
this axes using `~proplot.config.RcConfigurator.context`.
**kwargs
Passed to `Axes.format` or passed to `~proplot.config.RcConfigurator.context`
and used to update axes `~proplot.config.rc` settings. For example,
``abcstyle='A.'`` modifies the :rcraw:`abc.style` setting.
"""
docstring.snippets['axes.patch_kw'] = """
patch_kw : dict-like, optional
Keyword arguments used to update the background patch. This can
be used e.g. to apply background hatching with ``patch_kw={'hatch': 'xxx'}``.
"""
docstring.snippets['axes.proj'] = """
The map projection specification(s). If ``'cartesian'`` (the default), a
`~proplot.axes.CartesianAxes` is created. If ``'polar'``, a
`~proplot.axes.PolarAxes` is created. Otherwise, the argument is
interpreted by `~proplot.constructor.Proj`, and the result is used
to make a `~proplot.axes.GeoAxes` (in this case the argument can be
a `cartopy.crs.Projection` instance, a `~mpl_toolkits.basemap.Basemap`
instance, or a projection name listed in :ref:`this table <proj_table>`).
"""
docstring.snippets['axes.inset'] = """
Return an inset `CartesianAxes`. This is similar to the builtin
`~matplotlib.axes.Axes.inset_axes` but includes some extra 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. Default is
``'axes'``, i.e. `bounds` is in axes-relative coordinates.
proj, projection : str, `cartopy.crs.Projection`, or `~mpl_toolkits.basemap.Basemap`
The map projection specification(s). If not provided, the inset axes
projection is identical to the current axes projection. If ``'cartesian'``,
a `~proplot.axes.CartesianAxes` inset is created. If ``'polar'``, a
`~proplot.axes.PolarAxes` inset is created. Otherwise, the argument is
interpreted by `~proplot.constructor.Proj`, and the result is used
to make a `~proplot.axes.GeoAxes` (in this case the argument can be
a `cartopy.crs.Projection` instance, a `~mpl_toolkits.basemap.Basemap`
instance, or a projection name listed in :ref:`this table <proj_table>`).
proj_kw, projection_kw : dict-like, optional
Keyword arguments passed to `~mpl_toolkits.basemap.Basemap` or
cartopy `~cartopy.crs.Projection` classes on instantiation.
basemap : bool or dict-like, optional
Whether to use `~mpl_toolkits.basemap.Basemap` or
`~cartopy.crs.Projection` for map projections. Default is ``False``.
zorder : float, optional
The `zorder <https://matplotlib.org/stable/gallery/misc/zorder_demo.html>`__
of the axes, should be greater than the zorder of
elements in the parent axes. Default is ``4``.
zoom : bool, optional
Whether to draw lines indicating the inset zoom using
`~Axes.indicate_inset_zoom`. The lines will automatically
adjust whenever the parent axes or inset axes limits are changed.
Default is ``True``.
zoom_kw : dict, optional
Passed to `~Axes.indicate_inset_zoom`.
Other parameters
----------------
**kwargs
Passed to `CartesianAxes`.
""" % docstring.snippets
docstring.snippets['axes.panel'] = """
Return a panel drawn along the edge of this axes.
Parameters
----------
side : str, optional
The panel location. The following location keys are valid:
========== =====================
Location Valid keys
========== =====================
left ``'left'``, ``'l'``
right ``'right'``, ``'r'``
bottom ``'bottom'``, ``'b'``
top ``'top'``, ``'t'``
========== =====================
width : float or str or list thereof, optional
The panel width. Units are interpreted by `~proplot.utils.units`.
Default is :rc:`subplots.panelwidth`.
space : float or str or list thereof, optional
Empty space between the main subplot and the panel.
When :rcraw:`tight` is ``True``, this is adjusted automatically.
Otherwise, the default is :rc:`subplots.panelpad`.
share : bool, optional
Whether to enable axis sharing between the *x* and *y* axes of the
main subplot and the panel long axes for each panel in the stack.
Sharing between the panel short axis and other panel short axes
is determined by figure-wide `sharex` and `sharey` settings.
Returns
-------
`~proplot.axes.CartesianAxes`
The panel axes.
"""
class Axes(maxes.Axes):
"""
Lowest-level axes subclass. Handles titles and axis
sharing. Adds several new methods and overrides existing ones.
"""
def __init__(self, *args, number=None, main=False, _subplotspec=None, **kwargs):
"""
Parameters
----------
number : int
The subplot number, used for a-b-c labeling. See `~Axes.format`
for details. Note the first axes is ``1``, not ``0``.
main : bool, optional
Used internally, indicates whether this is a "main axes" rather
than a twin, panel, or inset axes.
Other parameters
----------------
*args, **kwargs
Passed to `~matplotlib.axes.Axes`.
See also
--------
matplotlib.axes.Axes
proplot.axes.CartesianAxes
proplot.axes.PolarAxes
proplot.axes.GeoAxes
"""
super().__init__(*args, **kwargs)
# Ensure isDefault_minloc enabled at start, needed for dual axes
self.xaxis.isDefault_minloc = self.yaxis.isDefault_minloc = True
# Properties
# TODO: Why are some of these not set in __init__?
if main:
self.figure._subplots_main.append(self)
self.number = number # for a-b-c numbering
self._auto_format = None # manipulated by wrapper functions
self._abc_loc = None
self._abc_text = None
self._abc_border_kwargs = {}
self._abc_pad = rc['abc.titlepad']
self._title_loc = None
self._title_border_kwargs = {} # title border properties
self._title_above = rc['title.above']
self._title_pad = rc['title.pad']
self._title_pad_current = None
self._tight_bbox = None # bounding boxes are saved
self._panel_hidden = False # True when "filled" with cbar/legend
self._panel_parent = None
self._panel_share = False
self._panel_sharex_group = False
self._panel_sharey_group = False
self._panel_side = None
self._inset_parent = None
self._inset_zoom = False
self._inset_zoom_data = None
# Axes colorbars and legends
self._colorbar_dict = {}
self._legend_dict = {}
# Axes panels
d = self._panel_dict = {}
d['left'] = [] # NOTE: panels will be sorted inside-to-outside
d['right'] = []
d['bottom'] = []
d['top'] = []
# Axes titles
# Record the original positions to account for offsetting
d = self._title_dict = {}
ta = self.transAxes
d['abc'] = self.text(0, 0, '', transform=ta)
d['left'] = self._left_title # WARNING: track in case mpl changes this
d['center'] = self.title
d['right'] = self._right_title
d['upper left'] = self.text(0, 0, '', va='top', ha='left', transform=ta)
d['upper center'] = self.text(0, 0, '', va='top', ha='center', transform=ta)
d['upper right'] = self.text(0, 0, '', va='top', ha='right', transform=ta)
d['lower left'] = self.text(0, 0, '', va='bottom', ha='left', transform=ta)
d['lower center'] = self.text(0, 0, '', va='bottom', ha='center', transform=ta)
d['lower right'] = self.text(0, 0, '', va='bottom', ha='right', transform=ta)
# Axes row and column labels
# NOTE: Most of these sit empty for most subplots
# TODO: Implement this with EdgeStack, avoid creating silly empty objects
d = self._label_dict = {}
tf = self.figure.transFigure
tc = mtransforms.blended_transform_factory(ta, tf)
tr = mtransforms.blended_transform_factory(tf, ta)
d['left'] = self.text(0, 0.5, '', va='center', ha='right', transform=tr)
d['right'] = self.text(0, 0.5, '', va='center', ha='left', transform=tr)
d['bottom'] = self.text(0.5, 0, '', va='top', ha='center', transform=tc)
d['top'] = self.text(0.5, 0, '', va='bottom', ha='center', transform=tc)
d = self._label_pad = {}
d['left'] = rc['leftlabel.pad']
d['right'] = rc['rightlabel.pad']
d['bottom'] = rc['bottomlabel.pad']
d['top'] = rc['toplabel.pad']
# Subplot spec
# WARNING: For mpl>=3.4.0 subplotspec assigned *after* initialization using
# set_subplotspec. Tried to defer to setter but really messes up both format()
# and _auto_share_setup(). Instead use workaround: Have Figure.add_subplot pass
# subplotspec as a hidden keyword arg. Non-subplots don't need this arg.
# See https://github.com/matplotlib/matplotlib/pull/18564
if _subplotspec is not None:
self.set_subplotspec(_subplotspec)
# Default sharing and formatting
# TODO: Apply specific setters instead of format()
self._auto_share_setup()
self.format(rc_mode=1) # rc_mode == 1 applies the custom proplot params
def _auto_share_setup(self):
"""
Automatically configure axis sharing based on the horizontal and
vertical extent of subplots in the figure gridspec.
"""
# Panel axes sharing, between main subplot and its panels
# NOTE: _panel_share means "include this panel in the axis sharing group"
# while _panel_sharex_group indicates the group itself and may include main axes
def shared(paxs):
return [pax for pax in paxs if not pax._panel_hidden and pax._panel_share]
# Internal axis sharing, share stacks of panels and main axes with each other
# NOTE: *This* block is why, even though share[xy] are figure-wide
# settings, we still need the axes-specific _share[xy]_override attr
if not self._panel_side: # this is a main axes
# Top and bottom
bottom = self
paxs = shared(self._panel_dict['bottom'])
if paxs:
bottom = paxs[-1]
bottom._panel_sharex_group = False
for iax in (self, *paxs[:-1]):
iax._panel_sharex_group = True
iax._sharex_setup(bottom) # parent is bottom-most
paxs = shared(self._panel_dict['top'])
for iax in paxs:
iax._panel_sharex_group = True
iax._sharex_setup(bottom)
# Left and right
# NOTE: Order of panel lists is always inside-to-outside
left = self
paxs = shared(self._panel_dict['left'])
if paxs:
left = paxs[-1]
left._panel_sharey_group = False
for iax in (self, *paxs[:-1]):
iax._panel_sharey_group = True
iax._sharey_setup(left) # parent is left-most
paxs = shared(self._panel_dict['right'])
for iax in paxs:
iax._panel_sharey_group = True
iax._sharey_setup(left)
# External axes sharing, sometimes overrides panel axes sharing
# NOTE: This can get very repetitive, but probably minimal impact?
# Share x axes
parent, *children = self._get_extent_axes('x')
for child in children:
child._sharex_setup(parent)
# Share y axes
parent, *children = self._get_extent_axes('y')
for child in children:
child._sharey_setup(parent)
def _get_extent_axes(self, x, panels=False):
"""
Return the axes whose horizontal or vertical extent in the main
gridspec matches the horizontal or vertical extend of this axes.
The lefmost or bottommost axes are at the start of the list.
"""
if not hasattr(self, 'get_subplotspec'):
return [self]
y = 'y' if x == 'x' else 'x'
idx = 0 if x == 'x' else 1
argfunc = np.argmax if x == 'x' else np.argmin
irange = self._range_gridspec(x)
if panels:
axs = self.figure._iter_axes(hidden=False, children=False)
else:
axs = self.figure._subplots_main
axs = [ax for ax in axs if ax._range_gridspec(x) == irange]
if not axs:
return [self]
else:
pax = axs.pop(argfunc([ax._range_gridspec(y)[idx] for ax in axs]))
return [pax, *axs]
def _get_side_axes(self, side, panels=False):
"""
Return the axes whose left, right, top, or bottom sides abutt
against the same row or column as this axes.
"""
if side not in ('left', 'right', 'bottom', 'top'):
raise ValueError(f'Invalid side {side!r}.')
if not hasattr(self, 'get_subplotspec'):
return [self]
x = 'x' if side in ('left', 'right') else 'y'
idx = 0 if side in ('left', 'top') else 1 # which side to test
coord = self._range_gridspec(x)[idx] # side for a particular axes
if panels:
axs = self.figure._iter_axes(hidden=False, children=False)
else:
axs = self.figure._subplots_main
axs = [ax for ax in axs if ax._range_gridspec(x)[idx] == coord]
if not axs:
return [self]
else:
return axs
def _hide_panel(self):
"""
Hide axes contents but do *not* make the entire axes invisible. This is used to
fill "panels" surreptitiously added to the gridspec for the purpose of drawing
outer colorbars and legends.
"""
# NOTE: Do not run self.clear in case we want to add a subplot title
# above a colorbar on a top panel (see _reassign_title).
for s in self.spines.values():
s.set_visible(False)
self.xaxis.set_visible(False)
self.yaxis.set_visible(False)
self.patch.set_alpha(0)
self._panel_hidden = True
def _is_panel(self):
"""
Return whether the current axes is a panel.
"""
return bool(self._panel_parent)
def _is_panel_group_member(self, other):
"""
Return whether the axes are related.
"""
return (
self._panel_parent is other # child
or other._panel_parent is self # parent
or other._panel_parent is self._panel_parent # sibling
)
def _loc_translate(self, loc, mode=None):
"""
Return the location string `loc` translated into a standardized form.
"""
if mode == 'legend':
options = tuple(LOC_TRANSLATE.values())
elif mode == 'panel':
options = ('left', 'right', 'top', 'bottom')
elif mode == 'colorbar':
options = (
'best', 'left', 'right', 'top', 'bottom',
'upper left', 'upper right', 'lower left', 'lower right',
)
elif mode in ('abc', 'title'):
options = (
'left', 'center', 'right',
'upper left', 'upper center', 'upper right',
'lower left', 'lower center', 'lower right',
)
else:
raise ValueError(f'Invalid mode {mode!r}.')
loc_translate = {
key: value
for short, long in LOC_TRANSLATE.items()
for key, value in ((long, long), (short, long))
if long in options
}
if loc in (None, True):
context = mode in ('abc', 'title')
loc = rc.get(mode + '.loc', context=context)
if loc is not None:
loc = self._loc_translate(loc, mode)
elif isinstance(loc, (str, Integral)):
try:
loc = loc_translate[loc]
except KeyError:
raise KeyError(
f'Invalid {mode} location {loc!r}. Options are: '
+ ', '.join(map(repr, loc_translate)) + '.'
)
elif (
mode == 'legend'
and np.iterable(loc)
and len(loc) == 2
and all(isinstance(l, Number) for l in loc)
):
loc = tuple(loc)
else:
raise KeyError(f'Invalid {mode} location {loc!r}.')
if mode == 'colorbar' and loc == 'best': # white lie
loc = 'lower right'
return loc
def _make_inset_locator(self, bounds, trans):
"""
Return a locator that determines inset axes bounds.
"""
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 _range_gridspec(self, x):
"""
Return the column or row gridspec range for the axes.
"""
if not hasattr(self, 'get_subplotspec'):
raise RuntimeError('Axes is not a subplot.')
ss = self.get_subplotspec()
if hasattr(ss, 'get_active_rows_columns'):
func = ss.get_active_rows_columns
else:
func = ss.get_rows_columns
if x == 'x':
_, _, _, _, col1, col2 = func()
return col1, col2
else:
_, _, row1, row2, _, _ = func()
return row1, row2
def _range_tightbbox(self, x):
"""
Return the tight bounding box span from the cached bounding box.
`~proplot.axes.Axes.get_tightbbox` caches bounding boxes when
`~Figure.get_tightbbox` is called.
"""
# TODO: Better testing for axes visibility
bbox = self._tight_bbox
if bbox is None:
return np.nan, np.nan
if x == 'x':
return bbox.xmin, bbox.xmax
else:
return bbox.ymin, bbox.ymax
def _reassign_label(self, side):
"""
Reassign the column and row labels to the relevant panel if present.
This is called by `~proplot.figure.Figure._align_subplot_figure_labels`.
"""
# NOTE: Since panel axes are "children" main axes is always drawn first.
paxs = self._panel_dict[side]
if not paxs:
return self
kw = {}
pax = paxs[-1] # outermost
cobj = self._label_dict[side]
pobj = pax._label_dict[side]
for key in ('text', 'color', 'fontproperties'):
kw[key] = getattr(cobj, 'get_' + key)()
pobj.update(kw)
cobj.set_text('')
return pax
def _reassign_title(self):
"""
Re-assign the title to the first upper panel if present. We cannot
simply add the upper panel as a child axes, because then the title will
be offset but still belong to main axes, which messes up the tight
bounding box.
"""
# NOTE: Since panel axes are "children" main axes is always drawn first.
taxs = self._panel_dict['top']
if not taxs or not self._title_above:
return
tax = taxs[-1] # outermost
tax._title_pad = self._title_pad
for loc in ('abc', 'left', 'center', 'right'):
kw = {}
cobj = self._title_dict[loc]
tobj = tax._title_dict[loc] # WARNING: Careful to use 'abc' here
if loc == 'abc':
loc = tax._abc_loc = self._abc_loc
if loc not in ('left', 'center', 'right'):
continue
text = cobj.get_text()
if not text:
continue
for key in ('color', 'fontproperties'):
kw[key] = getattr(cobj, 'get_' + key)()
tobj.update(kw)
tobj.set_text(text)
cobj.set_text('')
def _sharex_setup(self, sharex):
"""
Configure x-axis sharing for panels. Main axis sharing is done in
`~CartesianAxes._sharex_setup`.
"""
self._share_short_axis(sharex, 'left') # x axis of left panels
self._share_short_axis(sharex, 'right')
self._share_long_axis(sharex, 'bottom') # x axis of bottom panels
self._share_long_axis(sharex, 'top')
def _sharey_setup(self, sharey):
"""
Configure y-axis sharing for panels. Main axis sharing is done in
`~CartesianAxes._sharey_setup`.
"""
self._share_short_axis(sharey, 'bottom') # y axis of bottom panels
self._share_short_axis(sharey, 'top')
self._share_long_axis(sharey, 'left') # y axis of left panels
self._share_long_axis(sharey, 'right')
def _share_short_axis(self, share, side):
"""
Share the "short" axes of panels belonging to this subplot
with panels belonging to an external subplot.
"""
if share is None or self._panel_side:
return # if this is a panel
axis = 'x' if side in ('left', 'right') else 'y'
caxs = self._panel_dict[side]
paxs = share._panel_dict[side]
caxs = [pax for pax in caxs if not pax._panel_hidden]
paxs = [pax for pax in paxs if not pax._panel_hidden]
for cax, pax in zip(caxs, paxs): # may be uneven
getattr(cax, '_share' + axis + '_setup')(pax)
def _share_long_axis(self, share, side):
"""
Share the "long" axes of panels belonging to this subplot
with panels belonging to an external subplot.
"""
# NOTE: We do not check _panel_share because that only controls
# sharing with main subplot, not other subplots
if share is None or self._panel_side:
return # if this is a panel
axis = 'x' if side in ('top', 'bottom') else 'y'
paxs = self._panel_dict[side]
paxs = [pax for pax in paxs if not pax._panel_hidden]
for pax in paxs:
getattr(pax, '_share' + axis + '_setup')(share)
def _update_abc(self):
"""
Whether to update the label.
"""
abc = False
if self._panel_side:
return
# Properties
# NOTE: Border props only apply for "inner" title locations so we
# need to store on the axes whenever they are modified and always
# re-apply the ones stored on the axes.
kw = rc.fill(
{
'fontsize': 'abc.size',
'weight': 'abc.weight',
'color': 'abc.color',
'fontfamily': 'font.family',
},
context=True
)
kwb = rc.fill(
{
'border': 'abc.border',
'borderwidth': 'abc.borderwidth',
'bbox': 'abc.bbox',
'bboxpad': 'abc.bboxpad',
'bboxcolor': 'abc.bboxcolor',
'bboxstyle': 'abc.bboxstyle',
'bboxalpha': 'abc.bboxalpha',
},
context=True,
)
self._abc_border_kwargs.update(kwb)
kw.update(self._abc_border_kwargs)
# A-b-c labels. Build as a...z...aa...zz...aaa...zzz
style = rc.get('abc.style', context=True) # 1st run, or changed
if style and self.number is not None:
if not isinstance(style, str) or 'a' not in style and 'A' not in style:
raise ValueError(
f'Invalid abcstyle {style!r}. Must include letter "a" or "A".'
)
nabc, iabc = divmod(self.number - 1, 26)
old = re.search('[aA]', style).group() # return the *first* 'a'
new = (nabc + 1) * ABC_STRING[iabc]
new = new.upper() if old == 'A' else new
self._abc_text = style.replace(old, new, 1)
# Apply a-b-c text
abc = rc.get('abc', context=True)
aobj = self._title_dict['abc']
if abc is not None:
aobj.set_text(self._abc_text if bool(abc) else '')
# Apply a-b-c settings
loc = self._loc_translate(None, 'abc')
loc_prev = self._abc_loc
if loc is None:
loc = loc_prev
if loc in ('left', 'right', 'center'):
for key in KEYS_INNER:
kw.pop(key, None)
aobj.update(kw)
self._abc_loc = loc
def _update_super(self, suptitle, **kwargs):
"""
Update super title and row and column labels.
"""
# NOTE: These are actually *figure-wide* settings, but that line gets
# 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.
# NOTE: Below kludge prevents changed *figure-wide* settings from getting
# overwritten when user makes a new panels or insets. Funky limnitation but
# kind of makes sense if these are inaccessible from panels.
fig = self.figure
ignore = self not in fig._subplots_main
kw = {} if ignore else rc.fill(
{
'fontsize': 'suptitle.size',
'weight': 'suptitle.weight',
'color': 'suptitle.color',
'fontfamily': 'font.family'
},
context=True,
)
if suptitle or kw:
fig._update_super_title(suptitle, **kw)
# Labels
for side, labels in kwargs.items():
kw = {} if ignore else rc.fill(
{
'fontsize': side + 'label.size',
'weight': side + 'label.weight',
'color': side + 'label.color',
'fontfamily': 'font.family'
},
context=True,
)
if labels or kw:
fig._update_super_labels(self, side, labels, **kw)
def _update_title_all(self, title=None, **kwargs):
"""
Update the titles.
"""
# Titles, with two workflows here:
# 1. title='name' and titleloc='position'
# 2. ltitle='name', rtitle='name', etc., arbitrarily many titles
# NOTE: Matplotlib added axes.titlecolor in version 3.2 but we
# still use custom title.size, title.weight, title.color
# properties for retroactive support in older matplotlib versions.
# First get params and update kwargs
kw = rc.fill(
{
'fontsize': 'title.size',
'weight': 'title.weight',
'color': 'title.color',
'fontfamily': 'font.family',
},
context=True
)
if 'color' in kw and kw['color'] == 'auto':
del kw['color'] # WARNING: matplotlib permits invalid color here
kwb = rc.fill(
{
'border': 'title.border',
'borderwidth': 'title.borderwidth',
'bbox': 'title.bbox',
'bboxpad': 'title.bboxpad',
'bboxcolor': 'title.bboxcolor',
'bboxstyle': 'title.bboxstyle',
'bboxalpha': 'title.bboxalpha',
},
context=True,
)
self._title_border_kwargs.update(kwb)
kw.update(self._title_border_kwargs)
# Workflow 2, want this to come first so workflow 1 gets priority
for iloc, ititle in kwargs.items():
ikw = kw.copy()
iloc = self._loc_translate(iloc, 'title')
if iloc in ('left', 'center', 'right'):
for key in KEYS_INNER:
ikw.pop(key, None)
iobj = self._title_dict[iloc]
iobj.update(ikw)
if ititle is not None:
iobj.set_text(ititle)
# Workflow 1, make sure that if user calls ax.format(title='Title')
# *then* ax.format(titleloc='left') it copies over the text.
# Get current and previous location, prevent overwriting abc label
loc = self._loc_translate(None, 'title')
loc_prev = self._title_loc
if loc is None: # never None first run
loc = loc_prev # never None on subsequent runs
# Remove previous text
if loc_prev is not None and loc != loc_prev:
tobj_prev = self._title_dict[loc_prev]
if title is None:
title = tobj_prev.get_text()
tobj_prev.set_text('')
# Add new text and settings
kw = kw.copy()
if loc in ('left', 'center', 'right'):
for key in KEYS_INNER:
kw.pop(key, None)
tobj = self._title_dict[loc]
tobj.update(kw)
if title is not None:
tobj.set_text(title)
self._title_loc = loc # assigns default loc on first run
def _update_title_position(self, renderer):
"""
Update the position of proplot inset titles and builtin matplotlib
titles. This is called by matplotlib at drawtime.
"""
# Update title positions
# NOTE: Critical to do this every time in case padding changes or
# we added or removed an a-b-c label in the same position as a title
width, height = self.get_size_inches()
x_pad = self._title_pad / (72 * width)
y_pad = self._title_pad / (72 * height)
for loc, obj in self._title_dict.items():
x, y = (0, 1)
if loc == 'abc': # redirect
loc = self._abc_loc
if loc == 'left':
x = 0
elif loc == 'center':
x = 0.5
elif loc == 'right':
x = 1
if loc in ('upper center', 'lower center'):
x = 0.5
elif loc in ('upper left', 'lower left'):
x = x_pad
elif loc in ('upper right', 'lower right'):
x = 1 - x_pad
if loc in ('upper left', 'upper right', 'upper center'):
y = 1 - y_pad
elif loc in ('lower left', 'lower right', 'lower center'):
y = y_pad
obj.set_position((x, y))
# Push title above tick marks, since builtin algorithm seems to ignore them.
# This is known matplotlib problem but especially annoying with top panels.
# NOTE: See axis.get_ticks_position for inspiration
pad = self._title_pad
if self.xaxis.get_visible() and any(
tick.tick2line.get_visible() and not tick.label2.get_visible()
for tick in self.xaxis.majorTicks
):
pad += self.xaxis.get_tick_padding()
# Avoid applying padding on every draw in case it is expensive to change
# the title Text transforms every time.
pad_current = self._title_pad_current
if pad_current is None or not np.isclose(pad, pad_current):
self._title_pad_current = pad
self._set_title_offset_trans(pad)
# Adjust the above-axes positions with builtin algorithm
# WARNING: Make sure the name of this private function doesn't change
super()._update_title_position(renderer)
# Sync the title positiona with the a-b-c label position
aobj = self._title_dict['abc']
tobj = self._title_dict[self._abc_loc]
aobj.set_ha(tobj.get_ha())
aobj.set_va(tobj.get_va())
aobj.set_position(tobj.get_position())
aobj.set_transform(tobj.get_transform())
# Offset title away from a-b-c label
# NOTE: Title texts all use axes transform in x-direction
# TODO: Make empirical padding of '0.4' em tunable?
if not tobj.get_text() or not aobj.get_text():
return
awidth, twidth = (
obj.get_window_extent(renderer).transformed(self.transAxes.inverted()).width
for obj in (aobj, tobj)
)
apad, tpad = (
(self._abc_pad / 72) / self.get_size_inches()[0]
for obj in (aobj, tobj)
)
ha = aobj.get_ha()
aoffset = toffset = 0
if ha == 'left':
toffset = awidth + apad
elif ha == 'right':
aoffset = -(twidth + tpad)
else: # guaranteed center, there are others
toffset = 0.5 * (awidth + apad)
aoffset = -0.5 * (twidth + tpad)
aobj.set_x(aobj.get_position()[0] + aoffset)
tobj.set_x(tobj.get_position()[0] + toffset)
@staticmethod
@warnings._rename_kwargs('0.6', mode='rc_mode')
def _parse_format(rc_kw=None, rc_mode=None, **kwargs):
"""
Separate `~proplot.config.rc` setting name value pairs from
`~Axes.format` keyword arguments.
"""
kw = {}
rc_kw = rc_kw or {}
rc_mode = _not_none(rc_mode, 2)
for key, value in kwargs.items():
key_fixed = rcsetup._rc_nodots.get(key, None)
if key_fixed is None:
kw[key] = value
else:
rc_kw[key_fixed] = value
return rc_kw, rc_mode, kw
@docstring.add_snippets
def format(
self, *, title=None,
figtitle=None, suptitle=None, rowlabels=None, collabels=None,
leftlabels=None, rightlabels=None, toplabels=None, bottomlabels=None,
llabels=None, rlabels=None, tlabels=None, blabels=None,
ltitle=None, ctitle=None, rtitle=None,
ultitle=None, uctitle=None, urtitle=None,
lltitle=None, lctitle=None, lrtitle=None,
):
"""
Modify the axes title(s), the a-b-c label, row and column labels, and
the figure title. Called by the `~proplot.axes.CartesianAxes`,
`~proplot.axes.PolarAxes`, and `~proplot.axes.GeoAxes` ``format``
methods.
Parameters
----------
title : str, optional
The axes title.
abc : bool, optional
Whether to apply "a-b-c" subplot labeling based on the subplot
`~Axes.number`. If `~Axes.number` is greater than 26, the labels
will loop around to a, ..., z, aa, ..., zz, aaa, ..., zzz, etc.
Default is :rc:`abc`.
abcstyle : str, optional
String denoting the format of a-b-c labels containing the character
``a`` or ``A``. ``'a'`` is the default, but e.g. ``'a.'``,
``'a)'``, or ``'A'`` might also be desirable. Default is
:rc:`abc.style`.
abcloc, titleloc : str, optional
Strings indicating the location for the a-b-c label and
main title. The following locations keys are valid (defaults are
:rc:`abc.loc` and :rc:`title.loc`):
.. _title_table:
======================== ============================
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'``
======================== ============================
ltitle, ctitle, rtitle, ultitle, uctitle, urtitle, lltitle, lctitle, lrtitle \
: str, optional
Axes titles in specific positions. Works as an alternative to
``ax.format(title='title', titleloc='loc')`` and lets you specify
multiple title-like labels in a single subplot.
abcborder, titleborder : bool, optional
Whether to draw a white border around titles and a-b-c labels
positioned inside the axes. This can help them stand out on top
of artists plotted inside the axes. Defaults are
:rc:`abc.border` and :rc:`title.border`
abcbbox, titlebbox : bool, optional
Whether to draw a white bbox around titles and a-b-c labels
positioned inside the axes. This can help them stand out on top
of artists plotted inside the axes. Defaults are
:rc:`abc.bbox` and :rc:`title.bbox`
titlepad : float, optional
The padding for the inner and outer titles and a-b-c labels in
arbitrary units (default is points). Default is :rc:`title.pad`.
titleabove : bool, optional
Whether to try to put outer titles and a-b-c labels above panels,
colorbars, or legends that are above the axes. Default is :rc:`title.above`.
leftlabels, toplabels, rightlabels, bottomlabels : list of str, optional
Labels for the subplots lying along the left, top, right, and
bottom edges of the figure. The length of each list must match
the number of subplots along the corresponding edge.
rowlabels, collabels, llabels, tlabels, rlabels, blabels : list of str, optional
Aliases for `leftlabels`, `toplabels`, `leftlabels`, `toplabels`,
`rightlabels`, and `bottomlabels`.
leftlabelpad, toplabelpad, rightlabelpad, bottomlabelpad : float, optional
The padding between the labels and the axes content in arbitrary units
(default is points). Defaults are :rcraw:`leftlabel.pad`,
:rcraw:`toplabel.pad`, :rcraw:`rightlabel.pad`, and :rcraw:`bottomlabel.pad`
suptitle, figtitle : 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 an improvement on matplotlib's "super" title, which just
centers the text between figure edges.
suptitlepad : float, optional
The padding between the super title and the axes content in arbitrary
units (default is points). Default is :rcraw:`suptitle.pad`.
Other parameters
----------------
%(axes.other)s
Important
---------
The `abc`, `abcstyle`, `abcloc`, `titleloc`, and `titleabove` keywords and
the various `pad` keywords are :ref:`configuration settings <ug_config>`.
We explicitly document these arguments here because it is very common to change