Skip to content

Commit 6125828

Browse files
committed
Add autoformat as plot keyword arg
1 parent c7da963 commit 6125828

File tree

2 files changed

+94
-64
lines changed

2 files changed

+94
-64
lines changed

proplot/axes/base.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,7 @@ def __init__(self, *args, number=None, main=False, **kwargs):
208208
self.xaxis.isDefault_minloc = self.yaxis.isDefault_minloc = True
209209

210210
# Properties
211+
self._auto_format = None # manipulated by wrapper functions
211212
self._abc_loc = None
212213
self._abc_text = None
213214
self._abc_border_kwargs = {} # abs border properties
@@ -1793,9 +1794,9 @@ def number(self, num):
17931794
boxplot = _boxplot_wrapper(_standardize_1d(_cycle_changer(
17941795
maxes.Axes.boxplot
17951796
)))
1796-
violinplot = _violinplot_wrapper(_standardize_1d(_indicate_error(
1797-
_cycle_changer(maxes.Axes.violinplot)
1798-
)))
1797+
violinplot = _violinplot_wrapper(_standardize_1d(_indicate_error(_cycle_changer(
1798+
maxes.Axes.violinplot
1799+
))))
17991800
fill_between = _fill_between_wrapper(_standardize_1d(_cycle_changer(
18001801
maxes.Axes.fill_between
18011802
)))

proplot/axes/plot.py

Lines changed: 90 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,15 @@
5656
'vlines_wrapper',
5757
]
5858

59+
docstring.snippets['standardize.autoformat'] = """
60+
autoformat : bool, optional
61+
Whether *x* axis labels, *y* axis labels, axis formatters, axes titles,
62+
colorbar labels, and legend labels are automatically configured when
63+
a `~pandas.Series`, `~pandas.DataFrame` or `~xarray.DataArray` is passed
64+
to the plotting command. Default is the figure-wide
65+
`proplot.figure.Figure.autoformat` setting.
66+
"""
67+
5968
docstring.snippets['axes.cmap_changer'] = """
6069
cmap : colormap spec, optional
6170
The colormap specifer, passed to the `~proplot.constructor.Colormap`
@@ -380,7 +389,8 @@ def _axis_labels_title(data, axis=None, units=True):
380389
return data, str(label).strip()
381390

382391

383-
def standardize_1d(self, func, *args, **kwargs):
392+
@docstring.add_snippets
393+
def standardize_1d(self, func, *args, autoformat=None, **kwargs):
384394
"""
385395
Interpret positional arguments for the "1D" plotting methods so usage is
386396
consistent. This also optionally modifies the x axis label, y axis label,
@@ -397,17 +407,22 @@ def standardize_1d(self, func, *args, **kwargs):
397407
try to infer them from the metadata. Otherwise,
398408
``np.arange(0, data.shape[0])`` is used.
399409
400-
See also
401-
--------
402-
cycle_changer
410+
Parameters
411+
----------
412+
%(standardize.autoformat)s
413+
414+
See also
415+
--------
416+
cycle_changer
403417
404-
Note
405-
----
406-
This function wraps {methods}
407-
"""
418+
Note
419+
----
420+
This function wraps {methods}
421+
"""
408422
# Sanitize input
409423
# TODO: Add exceptions for methods other than 'hist'?
410424
name = func.__name__
425+
autoformat = _not_none(autoformat, self.figure._auto_format)
411426
_load_objects()
412427
if not args:
413428
return func(self, *args, **kwargs)
@@ -466,7 +481,7 @@ def standardize_1d(self, func, *args, **kwargs):
466481
kwargs['positions'] = xi
467482

468483
# Next handle labels if 'autoformat' is on
469-
if self.figure._auto_format:
484+
if autoformat:
470485
# Ylabel
471486
y, label = _axis_labels_title(y)
472487
if label: # for histogram, this label is used for *x* coordinates
@@ -495,13 +510,14 @@ def standardize_1d(self, func, *args, **kwargs):
495510
xmin, xmax = self.projection.lonmin, self.projection.lonmax
496511
for y in ys:
497512
# Ensure data is monotonic and falls within map bounds
498-
ix, iy = _enforce_bounds(*_standardize_latlon(x, y), xmin, xmax)
513+
ix, iy = _enforce_bounds(*_fix_latlon(x, y), xmin, xmax)
499514
iys.append(iy)
500515
x, ys = ix, iys
501516

502517
# WARNING: For some functions, e.g. boxplot and violinplot, we *require*
503518
# cycle_changer is also applied so it can strip 'x' input.
504-
return func(self, x, *ys, *args, **kwargs)
519+
with _state_context(self, _auto_format=autoformat):
520+
return func(self, x, *ys, *args, **kwargs)
505521

506522

507523
def _enforce_bounds(x, y, xmin, xmax):
@@ -554,7 +570,7 @@ def _interp_poles(y, Z):
554570
return y, Z
555571

556572

557-
def _standardize_latlon(x, y):
573+
def _fix_latlon(x, y):
558574
"""
559575
Ensure longitudes are monotonic and make `~numpy.ndarray` copies so the
560576
contents can be modified. Ignores 2D coordinate arrays.
@@ -576,7 +592,10 @@ def _standardize_latlon(x, y):
576592
return x, y
577593

578594

579-
def standardize_2d(self, func, *args, order='C', globe=False, **kwargs):
595+
@docstring.add_snippets
596+
def standardize_2d(
597+
self, func, *args, autoformat=None, order='C', globe=False, **kwargs
598+
):
580599
"""
581600
Interpret positional arguments for the "2D" plotting methods so usage is
582601
consistent. This also optionally modifies the x axis label, y axis label,
@@ -595,6 +614,7 @@ def standardize_2d(self, func, *args, order='C', globe=False, **kwargs):
595614
596615
Parameters
597616
----------
617+
%(standardize.autoformat)s
598618
order : {{'C', 'F'}}, optional
599619
If ``'C'``, arrays should be shaped as ``(y, x)``. If ``'F'``, arrays
600620
should be shaped as ``(x, y)``. Default is ``'C'``.
@@ -621,6 +641,7 @@ def standardize_2d(self, func, *args, order='C', globe=False, **kwargs):
621641
"""
622642
# Sanitize input
623643
name = func.__name__
644+
autoformat = _not_none(autoformat, self.figure._auto_format)
624645
_load_objects()
625646
if not args:
626647
return func(self, *args, **kwargs)
@@ -707,21 +728,24 @@ def standardize_2d(self, func, *args, order='C', globe=False, **kwargs):
707728
kw['yminorlocator'] = mticker.NullLocator()
708729

709730
# Handle labels if 'autoformat' is on
710-
if self.figure._auto_format:
731+
if autoformat:
711732
for key, xy in zip(('xlabel', 'ylabel'), (x, y)):
712733
_, label = _axis_labels_title(xy)
713734
if label:
714735
kw[key] = label
715-
if len(xy) > 1 and all(isinstance(xy, Number)
716-
for xy in xy[:2]) and xy[1] < xy[0]:
736+
if (
737+
len(xy) > 1
738+
and all(isinstance(xy, Number) for xy in xy[:2])
739+
and xy[1] < xy[0]
740+
):
717741
kw[key[0] + 'reverse'] = True
718742
if xi is not None:
719743
x = xi
720744
if yi is not None:
721745
y = yi
722746

723747
# Handle figure titles
724-
if self.figure._auto_format:
748+
if autoformat:
725749
_, colorbar_label = _axis_labels_title(Zs[0], units=True)
726750
_, title = _axis_labels_title(Zs[0], units=False)
727751
if title:
@@ -796,7 +820,7 @@ def standardize_2d(self, func, *args, order='C', globe=False, **kwargs):
796820
getattr(self, 'name', '') == 'cartopy'
797821
and isinstance(kwargs.get('transform', None), PlateCarree)
798822
):
799-
x, y = _standardize_latlon(x, y)
823+
x, y = _fix_latlon(x, y)
800824
ix, iZs = x, []
801825
for Z in Zs:
802826
if globe and x.ndim == 1 and y.ndim == 1:
@@ -805,7 +829,7 @@ def standardize_2d(self, func, *args, order='C', globe=False, **kwargs):
805829

806830
# Fix seams by ensuring circular coverage. Unlike basemap,
807831
# cartopy can plot across map edges.
808-
if (x[0] % 360) != ((x[-1] + 360) % 360):
832+
if x[0] % 360 != (x[-1] + 360) % 360:
809833
ix = ma.concatenate((x, [x[0] + 360]))
810834
Z = ma.concatenate((Z, Z[:, :1]), axis=1)
811835
iZs.append(Z)
@@ -815,7 +839,7 @@ def standardize_2d(self, func, *args, order='C', globe=False, **kwargs):
815839
elif getattr(self, 'name', '') == 'basemap' and kwargs.get('latlon', None):
816840
# Fix grid
817841
xmin, xmax = self.projection.lonmin, self.projection.lonmax
818-
x, y = _standardize_latlon(x, y)
842+
x, y = _fix_latlon(x, y)
819843
ix, iZs = x, []
820844
for Z in Zs:
821845
# Ensure data is within map bounds
@@ -868,7 +892,8 @@ def standardize_2d(self, func, *args, order='C', globe=False, **kwargs):
868892
# was stripped by globe=True.
869893
colorbar_kw = kwargs.pop('colorbar_kw', None) or {}
870894
colorbar_kw.setdefault('label', colorbar_label)
871-
return func(self, x, y, *Zs, colorbar_kw=colorbar_kw, **kwargs)
895+
with _state_context(self, _auto_format=autoformat):
896+
return func(self, x, y, *Zs, colorbar_kw=colorbar_kw, **kwargs)
872897

873898

874899
def _get_error_data(
@@ -2184,6 +2209,7 @@ def cycle_changer(
21842209
# NOTE: Requires standardize_1d wrapper before reaching this. Also note
21852210
# that the 'x' coordinates are sometimes ignored below.
21862211
name = func.__name__
2212+
autoformat = self._auto_format # possibly manipulated by standardize_[12]d
21872213
if not args:
21882214
return func(self, *args, **kwargs)
21892215
x, y, *args = args
@@ -2265,19 +2291,36 @@ def cycle_changer(
22652291
if key in prop_keys and prop is None: # if key in cycler and property unset
22662292
apply_from_cycler.add(key)
22672293

2268-
# Handle legend labels and
2294+
# Handle legend labels. Several scenarios:
2295+
# 1. Always prefer input labels
2296+
# 2. Always add labels if this is a *named* dimension.
2297+
# 3. Even if not *named* dimension add labels if labels are string
22692298
# WARNING: Most methods that accept 2D arrays use columns of data, but when
22702299
# pandas DataFrame passed to hist, boxplot, or violinplot, rows of data
22712300
# assumed! This is fixed in parse_1d by converting to values.
2301+
y1 = ys[0]
22722302
ncols = 1
22732303
labels = _not_none(values=values, labels=labels, label=label)
2304+
colorbar_legend_label = None # for colorbar or legend
22742305
if name in ('pie', 'boxplot', 'violinplot'):
22752306
if labels is not None:
2276-
kwargs['labels'] = labels
2307+
kwargs['labels'] = labels # error raised down the line
22772308
else:
2309+
# Get column count and sanitize labels
22782310
ncols = 1 if y.ndim == 1 else y.shape[1]
2279-
if labels is None or isinstance(labels, str):
2311+
if not np.iterable(labels) or isinstance(labels, str):
22802312
labels = [labels] * ncols
2313+
if len(labels) != ncols:
2314+
raise ValueError(
2315+
f'Got {ncols} columns in data array, but {len(labels)} labels.'
2316+
)
2317+
# Get automatic legend labels and legend title
2318+
if autoformat:
2319+
ilabels, colorbar_legend_label = _axis_labels_title(y1, axis=1)
2320+
ilabels = _to_ndarray(ilabels) # may be empty!
2321+
for i, (ilabel, label) in enumerate(zip(ilabels, labels)):
2322+
if label is None and (colorbar_legend_label or isinstance(ilabel, str)):
2323+
labels[i] = ilabel
22812324

22822325
# Get step size for bar plots
22832326
# WARNING: This will fail for non-numeric non-datetime64 singleton
@@ -2301,7 +2344,6 @@ def cycle_changer(
23012344

23022345
# Plot susccessive columns
23032346
objs = []
2304-
label_leg_cbar = None # for colorbar or legend
23052347
for i in range(ncols):
23062348
# Prop cycle properties
23072349
kw = kwargs.copy()
@@ -2318,66 +2360,51 @@ def cycle_changer(
23182360
kw[key] = value
23192361

23202362
# Get x coordinates for bar plot
2321-
x_col, y_first = x, ys[0] # samples
2363+
ix = x # samples
23222364
if name in ('bar',): # adjust
23232365
if not stacked:
23242366
offset = width * (i - 0.5 * (ncols - 1))
2325-
x_col = x + offset
2326-
elif stacked and y_first.ndim > 1:
2367+
ix = x + offset
2368+
elif stacked and y1.ndim > 1:
23272369
key = 'x' if barh else 'bottom'
2328-
kw[key] = _to_indexer(y_first)[:, :i].sum(axis=1)
2370+
kw[key] = _to_indexer(y1)[:, :i].sum(axis=1)
23292371

23302372
# Get y coordinates and labels
23312373
if name in ('pie', 'boxplot', 'violinplot'):
23322374
# Only ever have one y value, cannot have legend labs
2333-
ys_col = (y_first,)
2375+
iys = (y1,)
23342376

23352377
else:
23362378
# The coordinates
23372379
# WARNING: If stacked=True then we always *ignore* second
23382380
# argument passed to fill_between. Warning should be issued
23392381
# by fill_between_wrapper in this case.
23402382
if stacked and name in ('fill_between', 'fill_betweenx'):
2341-
ys_col = tuple(
2342-
y_first if y_first.ndim == 1
2343-
else _to_indexer(y_first)[:, :ii].sum(axis=1)
2383+
iys = tuple(
2384+
y1 if y1.ndim == 1
2385+
else _to_indexer(y1)[:, :ii].sum(axis=1)
23442386
for ii in (i, i + 1)
23452387
)
23462388
else:
2347-
ys_col = tuple(
2389+
iys = tuple(
23482390
y_i if y_i.ndim == 1 else _to_indexer(y_i)[:, i]
23492391
for y_i in ys
23502392
)
23512393

2352-
# Possible legend labels
2353-
# Several scenarios:
2354-
# 1. Always prefer input labels
2355-
# 2. Always add labels if this is a *named* dimension.
2356-
# 3. Even if not *named* dimension add labels if labels are string
2357-
if len(labels) != ncols:
2358-
raise ValueError(
2359-
f'Got {ncols} columns in data array, '
2360-
f'but {len(labels)} labels.'
2361-
)
2362-
label = labels[i] # input labels
2363-
labels_cols, label_leg_cbar = _axis_labels_title(y_first, axis=1)
2364-
labels_cols = _to_ndarray(labels_cols)
2365-
if label is None and (
2366-
label_leg_cbar or labels_cols.size and isinstance(labels_cols[i], str)
2367-
):
2368-
label = labels_cols[i]
2394+
# Add label for artist
2395+
label = labels[i]
23692396
if label is not None:
23702397
kw['label'] = label
23712398

23722399
# Build coordinate arguments
2373-
x_ys_col = ()
2400+
ixy = ()
23742401
if barh: # special case, use kwargs only!
2375-
kw.update({'bottom': x_col, 'width': ys_col[0]})
2402+
kw.update({'bottom': ix, 'width': iys[0]})
23762403
elif name in ('pie', 'hist', 'boxplot', 'violinplot'):
2377-
x_ys_col = ys_col
2404+
ixy = iys
23782405
else: # has x-coordinates, and maybe more than one y
2379-
x_ys_col = (x_col, *ys_col)
2380-
obj = func(self, *x_ys_col, *args, **kw)
2406+
ixy = (ix, *iys)
2407+
obj = func(self, *ixy, *args, **kw)
23812408
if isinstance(obj, (list, tuple)) and len(obj) == 1:
23822409
obj = obj[0]
23832410
objs.append(obj)
@@ -2392,8 +2419,8 @@ def cycle_changer(
23922419
# Add keywords
23932420
if loc != 'fill':
23942421
colorbar_kw.setdefault('loc', loc)
2395-
if label_leg_cbar:
2396-
colorbar_kw.setdefault('label', label_leg_cbar)
2422+
if colorbar_legend_label:
2423+
colorbar_kw.setdefault('label', colorbar_legend_label)
23972424
self._auto_colorbar[loc][1].update(colorbar_kw)
23982425

23992426
# Add legend
@@ -2409,8 +2436,8 @@ def cycle_changer(
24092436
# Add keywords
24102437
if loc != 'fill':
24112438
legend_kw.setdefault('loc', loc)
2412-
if label_leg_cbar:
2413-
legend_kw.setdefault('label', label_leg_cbar)
2439+
if colorbar_legend_label:
2440+
legend_kw.setdefault('label', colorbar_legend_label)
24142441
self._auto_legend[loc][1].update(legend_kw)
24152442

24162443
# Return
@@ -2745,6 +2772,7 @@ def cmap_changer(
27452772
of the color selections.
27462773
"""
27472774
name = func.__name__
2775+
autoformat = self._auto_format # possibly manipulated by standardize_[12]d
27482776
if not args:
27492777
return func(self, *args, **kwargs)
27502778

@@ -2948,7 +2976,8 @@ def cmap_changer(
29482976
# Optionally add colorbar
29492977
if colorbar:
29502978
loc = self._loc_translate(colorbar, 'colorbar', allow_manual=False)
2951-
if 'label' not in colorbar_kw and self.figure._auto_format:
2979+
label = colorbar_kw.pop('label', None)
2980+
if label is None and autoformat:
29522981
_, label = _axis_labels_title(Z_sample) # last one is data, we assume
29532982
if label:
29542983
colorbar_kw.setdefault('label', label)

0 commit comments

Comments
 (0)