@@ -87,6 +87,180 @@ def _load_objects():
8787}
8888
8989
90+ class _InsetColorbar (martist .Artist ):
91+ """
92+ Hidden class for inset colorbars.
93+ """
94+ # NOTE: Add this to matplotlib directly?
95+ # TODO: Write this! Features currently implemented in axes
96+ # colorbar method.
97+
98+
99+ class _CenteredLegend (martist .Artist ):
100+ """
101+ Hidden class for legends with centered rows.
102+ """
103+ # NOTE: Add this to matplotlib directly?
104+ # TODO: Embed entire "centered row" feature in this class instead
105+ # of in hacky legend wrapper!
106+ def __str__ (self ):
107+ return 'CenteredLegend'
108+
109+
110+ def __init__ (self , pairs , loc = None , ** kwargs ):
111+ """
112+ Parameters
113+ ----------
114+ pairs : None
115+ The legend pairs.
116+ loc : str, optional
117+ The legend location.
118+ fancybox : bool, optional
119+ Whether to use rectangle or rounded box.
120+ **kwargs
121+ Passed to `~matplotlib.legend.Legend`.
122+ """
123+ # Legend location
124+ loc = _notNone (loc , 'upper center' )
125+ if not isinstance (loc , str ):
126+ raise ValueError (
127+ f'Invalid location { loc !r} for legend with center=True. '
128+ 'Must be a location *string*.' )
129+ elif loc == 'best' :
130+ warnings .warn (
131+ 'For centered-row legends, cannot use "best" location. '
132+ 'Defaulting to "upper center".' )
133+
134+ # Determine space we want sub-legend to occupy as fraction of height
135+ # NOTE: Empirical testing shows spacing fudge factor necessary to
136+ # exactly replicate the spacing of standard aligned legends.
137+ fontsize = kwargs .get ('fontsize' , None ) or rc ['legend.fontsize' ]
138+ spacing = kwargs .get ('labelspacing' , None ) or rc ['legend.labelspacing' ]
139+ interval = 1 / len (pairs ) # split up axes
140+ interval = (((1 + spacing * 0.85 ) * fontsize ) / 72 ) / height
141+ for i , ipairs in enumerate (pairs ):
142+ if i == 1 :
143+ kwargs .pop ('title' , None )
144+ if i >= 1 and title is not None :
145+ i += 1 # extra space!
146+
147+ # Legend position
148+ if 'upper' in loc :
149+ y1 = 1 - (i + 1 ) * interval
150+ y2 = 1 - i * interval
151+ elif 'lower' in loc :
152+ y1 = (len (pairs ) + i - 2 ) * interval
153+ y2 = (len (pairs ) + i - 1 ) * interval
154+ else : # center
155+ y1 = 0.5 + interval * len (pairs ) / 2 - (i + 1 ) * interval
156+ y2 = 0.5 + interval * len (pairs ) / 2 - i * interval
157+ ymin = min (y1 , _notNone (ymin , y1 ))
158+ ymax = max (y2 , _notNone (ymax , y2 ))
159+
160+ # Draw legend
161+ bbox = mtransforms .Bbox ([[0 , y1 ], [1 , y2 ]])
162+ leg = mlegend .Legend (
163+ self , * zip (* ipairs ), loc = loc , ncol = len (ipairs ),
164+ bbox_transform = self .transAxes , bbox_to_anchor = bbox ,
165+ frameon = False , ** kwargs )
166+ legs .append (leg )
167+
168+ # Store legend and add frame
169+ self .leg = legs
170+ if not frameon :
171+ return
172+ if len (legs ) == 1 :
173+ legs [0 ].set_frame_on (True ) # easy!
174+ return
175+
176+ # Draw legend frame encompassing centered rows
177+ facecolor = _notNone (facecolor , rcParams ['legend.facecolor' ])
178+ if facecolor == 'inherit' :
179+ facecolor = rcParams ['axes.facecolor' ]
180+ self .legendPatch = FancyBboxPatch (
181+ xy = (0.0 , 0.0 ), width = 1.0 , height = 1.0 ,
182+ facecolor = facecolor ,
183+ edgecolor = edgecolor ,
184+ mutation_scale = fontsize ,
185+ transform = self .transAxes ,
186+ snap = True
187+ )
188+
189+ # Box style
190+ if fancybox is None :
191+ fancybox = rcParams ['legend.fancybox' ]
192+ if fancybox :
193+ self .legendPatch .set_boxstyle ('round' , pad = 0 , rounding_size = 0.2 )
194+ else :
195+ self .legendPatch .set_boxstyle ('square' , pad = 0 )
196+ self ._set_artist_props (self .legendPatch )
197+ self ._drawFrame = frameon
198+
199+ # Initialize with null renderer
200+ self ._init_legend_box (handles , labels , markerfirst )
201+
202+ # If shadow is activated use framealpha if not
203+ # explicitly passed. See Issue 8943
204+ if framealpha is None :
205+ if shadow :
206+ self .get_frame ().set_alpha (1 )
207+ else :
208+ self .get_frame ().set_alpha (rcParams ['legend.framealpha' ])
209+ else :
210+ self .get_frame ().set_alpha (framealpha )
211+
212+ if kwargs .get ('fancybox' , rc ['legend.fancybox' ]):
213+ patch .set_boxstyle ('round' , pad = 0 , rounding_size = 0.2 )
214+ else :
215+ patch .set_boxstyle ('square' , pad = 0 )
216+ patch .set_clip_on (False )
217+ patch .update (outline )
218+ self .add_artist (patch )
219+ # Add shadow
220+ # TODO: This does not work, figure out
221+ if kwargs .get ('shadow' , rc ['legend.shadow' ]):
222+ shadow = mpatches .Shadow (patch , 20 , - 20 )
223+ self .add_artist (shadow )
224+ # Add patch to list
225+ legs = (patch , * legs )
226+
227+
228+ def draw (renderer ):
229+ """
230+ Draw the legend and the patch.
231+ """
232+ for leg in legs :
233+ leg .draw (renderer )
234+
235+ renderer .open_group ('legend' )
236+ fontsize = renderer .points_to_pixels (self ._fontsize )
237+
238+ # if mode == fill, set the width of the legend_box to the
239+ # width of the parent (minus pads)
240+ if self ._mode in ['expand' ]:
241+ pad = 2 * (self .borderaxespad + self .borderpad ) * fontsize
242+ self ._legend_box .set_width (self .get_bbox_to_anchor ().width - pad )
243+
244+ # update the location and size of the legend. This needs to
245+ # be done in any case to clip the figure right.
246+ bbox = self ._legend_box .get_window_extent (renderer )
247+ self .legendPatch .set_bounds (bbox .x0 , bbox .y0 ,
248+ bbox .width , bbox .height )
249+ self .legendPatch .set_mutation_scale (fontsize )
250+
251+ if self ._drawFrame :
252+ if self .shadow :
253+ shadow = Shadow (self .legendPatch , 2 , - 2 )
254+ shadow .draw (renderer )
255+
256+ self .legendPatch .draw (renderer )
257+
258+ self ._legend_box .draw (renderer )
259+
260+ renderer .close_group ('legend' )
261+ self .stale = False
262+
263+
90264def default_latlon (self , func , * args , latlon = True , ** kwargs ):
91265 """
92266 Wraps %(methods)s for `~proplot.axes.BasemapAxes`.
@@ -2203,7 +2377,7 @@ def legend_wrapper(
22032377 raise ValueError (
22042378 f'Invalid order { order !r} . Choose from '
22052379 '"C" (row-major, default) and "F" (column-major).' )
2206- # may still be None, wait till later
2380+ # May still be None, wait till later
22072381 ncol = _notNone (ncols , ncol , None , names = ('ncols' , 'ncol' ))
22082382 title = _notNone (label , title , None , names = ('label' , 'title' ))
22092383 frameon = _notNone (
@@ -2259,8 +2433,7 @@ def legend_wrapper(
22592433 # This allows alternative workflow where user specifies labels when
22602434 # creating the legend.
22612435 pairs = []
2262- # e.g. not including BarContainer
2263- list_of_lists = (not hasattr (handles [0 ], 'get_label' ))
2436+ list_of_lists = (not hasattr (handles [0 ], 'get_label' )) # e.g. BarContainer
22642437 if labels is None :
22652438 for handle in handles :
22662439 if list_of_lists :
@@ -2322,17 +2495,16 @@ def legend_wrapper(
23222495 width , height = self .get_size_inches ()
23232496 # Individual legend
23242497 if not center :
2325- # Optionally change order
2498+ # Change order
23262499 # See: https://stackoverflow.com/q/10101141/4970632
23272500 # Example: If 5 columns, but final row length 3, columns 0-2 have
23282501 # N rows but 3-4 have N-1 rows.
23292502 ncol = _notNone (ncol , 3 )
23302503 if order == 'C' :
23312504 fpairs = []
2332- # split into rows
2333- split = [pairs [i * ncol :(i + 1 ) * ncol ]
2505+ split = [pairs [i * ncol :(i + 1 ) * ncol ] # split into rows
23342506 for i in range (len (pairs ) // ncol + 1 )]
2335- # max possible row count, and columns in final row
2507+ # Max possible row count, and columns in final row
23362508 nrowsmax , nfinalrow = len (split ), len (split [- 1 ])
23372509 nrows = [nrowsmax ] * nfinalrow + \
23382510 [nrowsmax - 1 ] * (ncol - nfinalrow )
@@ -2440,47 +2612,6 @@ def legend_wrapper(
24402612 for obj in leg .get_texts ():
24412613 if isinstance (obj , martist .Artist ):
24422614 obj .update (kw_text )
2443- # Draw manual fancy bounding box for un-aligned legend
2444- # WARNING: The matplotlib legendPatch transform is the default transform,
2445- # i.e. universal coordinates in points. Means we have to transform
2446- # mutation scale into transAxes sizes.
2447- # WARNING: Tempting to use legendPatch for everything but for some reason
2448- # coordinates are messed up. In some tests all coordinates were just result
2449- # of get window extent multiplied by 2 (???). Anyway actual box is found in
2450- # _legend_box attribute, which is accessed by get_window_extent.
2451- if center and frameon :
2452- if len (legs ) == 1 :
2453- legs [0 ].set_frame_on (True ) # easy!
2454- else :
2455- # Get coordinates
2456- renderer = self .figure .canvas .get_renderer ()
2457- bboxs = [leg .get_window_extent (renderer ).transformed (
2458- self .transAxes .inverted ()) for leg in legs ]
2459- xmin , xmax = min (bbox .xmin for bbox in bboxs ), max (
2460- bbox .xmax for bbox in bboxs )
2461- ymin , ymax = min (bbox .ymin for bbox in bboxs ), max (
2462- bbox .ymax for bbox in bboxs )
2463- fontsize = (fontsize / 72 ) / width # axes relative units
2464- fontsize = renderer .points_to_pixels (fontsize )
2465- # Draw and format patch
2466- patch = mpatches .FancyBboxPatch (
2467- (xmin , ymin ), xmax - xmin , ymax - ymin ,
2468- snap = True , zorder = 4.5 ,
2469- mutation_scale = fontsize , transform = self .transAxes )
2470- if kwargs .get ('fancybox' , rc ['legend.fancybox' ]):
2471- patch .set_boxstyle ('round' , pad = 0 , rounding_size = 0.2 )
2472- else :
2473- patch .set_boxstyle ('square' , pad = 0 )
2474- patch .set_clip_on (False )
2475- patch .update (outline )
2476- self .add_artist (patch )
2477- # Add shadow
2478- # TODO: This does not work, figure out
2479- if kwargs .get ('shadow' , rc ['legend.shadow' ]):
2480- shadow = mpatches .Shadow (patch , 20 , - 20 )
2481- self .add_artist (shadow )
2482- # Add patch to list
2483- legs = (patch , * legs )
24842615 # Append attributes and return, and set clip property!!! This is critical
24852616 # for tight bounding box calcs!
24862617 for leg in legs :
0 commit comments