Skip to content

Commit 83c2405

Browse files
committed
Initial commit, begin writing CenteredLegend
1 parent 11a416a commit 83c2405

File tree

2 files changed

+182
-49
lines changed

2 files changed

+182
-49
lines changed

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,10 @@ dist
1313
docs/api
1414
docs/_build
1515

16+
# Folder of notebooks for testing and bugfixing
17+
local
18+
1619
# Notebook stuff
17-
**/tests/*.ipynb
1820
.ipynb_checkpoints
1921

2022
# Python extras

proplot/wrappers.py

Lines changed: 179 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
90264
def 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

Comments
 (0)