33Utilties related to legends and colorbars.
44"""
55import matplotlib .artist as martist
6+ import matplotlib .axes as maxes
67import matplotlib .colorbar as mcolorbar
7- import matplotlib .legend as mlegend # noqa: F401
8+ import matplotlib .offsetbox as moffsetbox
9+ import matplotlib .projections as mprojections # noqa: F401
810import matplotlib .ticker as mticker
11+ import matplotlib .transforms as mtransforms
912import numpy as np
1013
1114from . import ic # noqa: F401
1215from . import warnings
1316
1417
15- def _fill_guide_kw (kwargs , ** pairs ):
18+ def _fill_guide_kw (kwargs , overwrite = False , ** pairs ):
1619 """
1720 Add the keyword arguments to the dictionary if not already present.
1821 """
@@ -25,42 +28,48 @@ def _fill_guide_kw(kwargs, **pairs):
2528 if value is None :
2629 continue
2730 keys = tuple (a for group in aliases for a in group if key in group ) # may be ()
28- if not any (kwargs .get (key ) is not None for key in keys ): # note any(()) is True
31+ keys_found = tuple (key for key in keys if kwargs .get (key ) is not None )
32+ if not keys_found :
2933 kwargs [key ] = value
34+ elif overwrite : # overwrite existing key
35+ kwargs [keys_found [0 ]] = value
3036
3137
32- def _guide_kw_from_obj ( obj , name , kwargs ):
38+ def _guide_kw_to_arg ( name , kwargs , ** pairs ):
3339 """
34- Add to the dict from settings stored on the object if there are no conflicts.
40+ Add to the `colorbar_kw` or `legend_kw` dict if there are no conflicts.
3541 """
36- pairs = getattr (obj , f'_{ name } _kw' , None )
37- pairs = pairs or {} # needed for some reason
38- _fill_guide_kw (kwargs , ** pairs )
39- if isinstance (obj , (tuple , list , np .ndarray )):
40- for iobj in obj : # possibly iterate over matplotlib tuple/list subclasses
41- _guide_kw_from_obj (iobj , name , kwargs )
42- return kwargs
42+ kw = kwargs .setdefault (f'{ name } _kw' , {})
43+ _fill_guide_kw (kw , overwrite = True , ** pairs )
4344
4445
4546def _guide_kw_to_obj (obj , name , kwargs ):
4647 """
47- Add the guide keyword dict to the objects .
48+ Store settings on the object from the input dict .
4849 """
50+ pairs = getattr (obj , f'_{ name } _kw' , None )
51+ pairs = pairs or {}
52+ _fill_guide_kw (pairs , overwrite = True , ** kwargs ) # update with current input
4953 try :
5054 setattr (obj , f'_{ name } _kw' , kwargs )
5155 except AttributeError :
5256 pass
5357 if isinstance (obj , (tuple , list , np .ndarray )):
54- for iobj in obj :
55- _guide_kw_to_obj (iobj , name , kwargs )
58+ for member in obj :
59+ _guide_kw_to_obj (member , name , kwargs )
5660
5761
58- def _guide_kw_to_arg ( name , kwargs , ** pairs ):
62+ def _guide_obj_to_kw ( obj , name , kwargs ):
5963 """
60- Add to the `colorbar_kw` or `legend_kw` dict if there are no conflicts.
64+ Add to the dict from settings stored on the object if there are no conflicts.
6165 """
62- kw = kwargs .setdefault (f'{ name } _kw' , {})
63- _fill_guide_kw (kw , ** pairs )
66+ pairs = getattr (obj , f'_{ name } _kw' , None )
67+ pairs = pairs or {}
68+ _fill_guide_kw (kwargs , overwrite = False , ** pairs ) # update from previous input
69+ if isinstance (obj , (tuple , list , np .ndarray )):
70+ for member in obj : # possibly iterate over matplotlib tuple/list subclasses
71+ _guide_obj_to_kw (member , name , kwargs )
72+ return kwargs
6473
6574
6675def _iter_children (* args ):
@@ -117,15 +126,93 @@ def _update_ticks(self, manual_only=False):
117126 self .minorticks_on () # at least turn them on
118127
119128
120- class _InsetColorbar (martist .Artist ):
121- """
122- Legend-like class for managing inset colorbars.
123- """
124- # TODO: Write this!
129+ class _AnchoredAxes (moffsetbox .AnchoredOffsetbox ):
130+ """
131+ An anchored child axes whose background patch and offset position is determined
132+ by the tight bounding box. Analogous to `~matplotlib.offsetbox.AnchoredText`.
133+ """
134+ def __init__ (self , ax , width , height , ** kwargs ):
135+ # Note the default bbox_to_anchor will be
136+ # the axes bounding box.
137+ bounds = [0 , 0 , 1 , 1 ] # arbitrary initial bounds
138+ child = maxes .Axes (ax .figure , bounds , zorder = self .zorder )
139+ # cls = mprojections.get_projection_class('proplot_cartesian') # TODO
140+ # child = cls(ax.figure, bounds, zorder=self.zorder)
141+ super ().__init__ (child = child , bbox_to_anchor = ax .bbox , ** kwargs )
142+ ax .add_artist (self ) # sets self.axes to ax and bbox_to_anchor to ax.bbox
143+ self ._child = child # ensure private attribute exists
144+ self ._width = width
145+ self ._height = height
146+
147+ def draw (self , renderer ):
148+ # Just draw the patch (not the axes)
149+ if not self .get_visible ():
150+ return
151+ if hasattr (self , '_update_offset_func' ):
152+ self ._update_offset_func (renderer )
153+ else :
154+ warnings ._warn_proplot (
155+ 'Failed to update _AnchoredAxes offset function due to matplotlib '
156+ 'private API change. The resulting axes position may be incorrect.'
157+ )
158+ bbox = self .get_window_extent (renderer )
159+ self ._update_patch (renderer , bbox = bbox )
160+ bbox = self .get_child_extent (renderer , offset = True )
161+ self ._update_child (bbox )
162+ self .patch .draw (renderer )
163+ self ._child .draw (renderer )
164+
165+ def _update_child (self , bbox ):
166+ # Update the child bounding box
167+ trans = getattr (self .figure , 'transSubfigure' , self .figure .transFigure )
168+ bbox = mtransforms .TransformedBbox (bbox , trans .inverted ())
169+ getattr (self ._child , '_set_position' , self ._child .set_position )(bbox )
170+
171+ def _update_patch (self , renderer , bbox ):
172+ # Update the patch position
173+ fontsize = renderer .points_to_pixels (self .prop .get_size_in_points ())
174+ self .patch .set_bounds (bbox .x0 , bbox .y0 , bbox .width , bbox .height )
175+ self .patch .set_mutation_scale (fontsize )
176+
177+ def get_extent (self , renderer , offset = False ):
178+ # Return the extent of the child plus padding
179+ fontsize = renderer .points_to_pixels (self .prop .get_size_in_points ())
180+ pad = self .pad * fontsize
181+ bbox = self ._child ._tight_bbox = self ._child .get_tightbbox (renderer )
182+ # bbox = self._child.get_tightbbox(renderer, use_cache=True) # TODO
183+ width = bbox .width + 2 * pad
184+ height = bbox .height + 2 * pad
185+ xd = yd = pad
186+ if offset :
187+ xd += self ._child .bbox .x0 - bbox .x0
188+ yd += self ._child .bbox .y0 - bbox .y0
189+ return width , height , xd , yd
190+
191+ def get_child_extent (self , renderer , offset = False ):
192+ # Update the child position
193+ fontsize = renderer .points_to_pixels (self .prop .get_size_in_points ())
194+ x0 , y0 = self ._child .bbox .x0 , self ._child .bbox .y0
195+ if offset : # find offset position
196+ self ._update_child (self .get_child_extent (renderer ))
197+ width , height , xd , yd = self .get_extent (renderer , offset = True )
198+ x0 , y0 = self .get_offset (width , height , xd , yd , renderer )
199+ # bbox = self._child.get_tightbbox(use_cache=True) # TODO
200+ xd += self ._child .bbox .x0 - self ._child ._tight_bbox .x0
201+ yd += self ._child .bbox .y0 - self ._child ._tight_bbox .y0
202+ width , height = self ._width * fontsize , self ._height * fontsize
203+ return mtransforms .Bbox .from_bounds (x0 , y0 , width , height )
204+
205+ def get_window_extent (self , renderer ):
206+ # Return the window bounding box
207+ self ._child .get_tightbbox (renderer ) # reset the cache
208+ self ._update_child (self .get_child_extent (renderer ))
209+ xi , yi , xd , yd = self .get_extent (renderer , offset = False )
210+ ox , oy = self .get_offset (xi , yi , xd , yd , renderer )
211+ return mtransforms .Bbox .from_bounds (ox - xd , oy - yd , xi , yi )
125212
126213
127214class _CenteredLegend (martist .Artist ):
128215 """
129- Legend-like class for managing centered-row legends.
216+ A legend-like subclass whose handles are grouped into centered rows of
217+ `~matplotlib.offsetbox.HPacker` rather than `~matplotlib.offsetbox.VPacker` columns.
130218 """
131- # TODO: Write this!
0 commit comments