Skip to content

Fix grouped bar plots with datetime64 x coords #156

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
May 10, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ There are quite a lot of deprecations for this release. Since this is

.. rubric:: Bug fixes

- Fix issue drawing bar plots with datetime *x* axes (:pr:`156`).
- Fix issue where `~proplot.ticker.AutoFormatter` tools were not locale-aware, i.e. use
comma as decimal point sometimes (:commit:`c7636296`).
- Fix issue where `~proplot.ticker.AutoFormatter` nonzero-value correction algorithm was
Expand Down
68 changes: 44 additions & 24 deletions proplot/wrappers.py
Original file line number Diff line number Diff line change
Expand Up @@ -262,8 +262,8 @@ def _axis_labels_title(data, axis=None, units=True):

def standardize_1d(self, func, *args, **kwargs):
"""
Interprets positional arguments for the "1d" plotting methods
%(methods)s. This also optionally modifies the x axis label, y axis label,
Interpret positional arguments for the "1d" plotting methods so usage is
consistent. This also optionally modifies the x axis label, y axis label,
title, and axis ticks if a `~xarray.DataArray`, `~pandas.DataFrame`, or
`~pandas.Series` is passed.

Expand All @@ -280,6 +280,10 @@ def standardize_1d(self, func, *args, **kwargs):
See also
--------
cycle_changer

Note
----
This function wraps the 1d plotting methods: %(methods)s.
"""
# Sanitize input
# TODO: Add exceptions for methods other than 'hist'?
Expand Down Expand Up @@ -311,8 +315,10 @@ def standardize_1d(self, func, *args, **kwargs):
# Auto x coords
y = ys[0] # test the first y input
if x is None:
axis = 1 if (name in ('hist', 'boxplot', 'violinplot') or any(
kwargs.get(s, None) for s in ('means', 'medians'))) else 0
axis = int(
name in ('hist', 'boxplot', 'violinplot')
or any(kwargs.get(s, None) for s in ('means', 'medians'))
)
x, _ = _axis_labels_title(y, axis=axis)
x = _to_array(x)
if x.ndim != 1:
Expand Down Expand Up @@ -452,8 +458,8 @@ def _standardize_latlon(x, y):

def standardize_2d(self, func, *args, order='C', globe=False, **kwargs):
"""
Interprets positional arguments for the "2d" plotting methods
%(methods)s. This also optionally modifies the x axis label, y axis label,
Interpret positional arguments for the "2d" plotting methods so usage is
consistent. This also optionally modifies the x axis label, y axis label,
title, and axis ticks if a `~xarray.DataArray`, `~pandas.DataFrame`, or
`~pandas.Series` is passed.

Expand Down Expand Up @@ -484,6 +490,10 @@ def standardize_2d(self, func, *args, order='C', globe=False, **kwargs):
See also
--------
cmap_changer

Note
----
This function wraps the 2d plotting methods: %(methods)s.
"""
# Sanitize input
name = func.__name__
Expand Down Expand Up @@ -587,10 +597,7 @@ def standardize_2d(self, func, *args, order='C', globe=False, **kwargs):
f'Input arrays must be 2d, instead got shape {Z.shape}.'
)
elif Z.shape[1] == xlen and Z.shape[0] == ylen:
if all(
z.ndim == 1 and z.size > 1
and _is_number(z) for z in (x, y)
):
if all(z.ndim == 1 and z.size > 1 and _is_number(z) for z in (x, y)):
x = edges(x)
y = edges(y)
else:
Expand All @@ -610,6 +617,7 @@ def standardize_2d(self, func, *args, order='C', globe=False, **kwargs):
f'Z centers {Z.shape} or '
f'Z borders {tuple(i+1 for i in Z.shape)}.'
)

# Optionally re-order
# TODO: Double check this
if order == 'F':
Expand All @@ -632,33 +640,27 @@ def standardize_2d(self, func, *args, order='C', globe=False, **kwargs):
f'Input arrays must be 2d, instead got shape {Z.shape}.'
)
elif Z.shape[1] == xlen - 1 and Z.shape[0] == ylen - 1:
if all(
z.ndim == 1 and z.size > 1
and _is_number(z) for z in (x, y)
):
if all(z.ndim == 1 and z.size > 1 and _is_number(z) for z in (x, y)):
x = (x[1:] + x[:-1]) / 2
y = (y[1:] + y[:-1]) / 2
else:
if (
x.ndim == 2 and x.shape[0] > 1 and x.shape[1] > 1
and _is_number(x)
):
x = 0.25 * (
x[:-1, :-1] + x[:-1, 1:] + x[1:, :-1] + x[1:, 1:]
)
x = 0.25 * (x[:-1, :-1] + x[:-1, 1:] + x[1:, :-1] + x[1:, 1:])
if (
y.ndim == 2 and y.shape[0] > 1 and y.shape[1] > 1
and _is_number(y)
):
y = 0.25 * (
y[:-1, :-1] + y[:-1, 1:] + y[1:, :-1] + y[1:, 1:]
)
y = 0.25 * (y[:-1, :-1] + y[:-1, 1:] + y[1:, :-1] + y[1:, 1:])
elif Z.shape[1] != xlen or Z.shape[0] != ylen:
raise ValueError(
f'Input shapes x {x.shape} and y {y.shape} '
f'must match Z centers {Z.shape} '
f'or Z borders {tuple(i+1 for i in Z.shape)}.'
)

# Optionally re-order
# TODO: Double check this
if order == 'F':
Expand Down Expand Up @@ -1819,13 +1821,31 @@ def cycle_changer(
key = 'edgecolors'
kw[key] = value

# Get x coordinates
x_col, y_first = x, ys[0] # samples
# Add x coordinates as pi chart labels by default
if name in ('pie',):
kw['labels'] = _not_none(labels, x_col) # TODO: move to pie wrapper?
kw['labels'] = _not_none(labels, x) # TODO: move to pie wrapper?

# Step size for grouped bar plots
# WARNING: This will fail for non-numeric non-datetime64 singleton
# datatypes but this is good enough for vast majority of most cases.
x_test = np.atleast_1d(_to_ndarray(x))
if len(x_test) >= 2:
x_step = x_test[1:] - x_test[:-1]
x_step = np.concatenate((x_step, x_step[-1:]))
elif x_test.dtype == np.datetime64:
x_step = np.timedelta64(1, 'D')
else:
x_step = np.array(0.5)
if np.issubdtype(x_test.dtype, np.datetime64):
x_step = x_step.astype('timedelta64[ns]') # avoid int timedelta truncation

# Get x coordinates for bar plot
x_col, y_first = x, ys[0] # samples
if name in ('bar',): # adjust
if not stacked:
x_col = x + (i - ncols / 2 + 0.5) * width / ncols
scale = i - 0.5 * (ncols - 1) # offset from true coordinate
scale = width * scale / ncols
x_col = x + x_step * scale
elif stacked and y_first.ndim > 1:
key = 'x' if barh else 'bottom'
kw[key] = _to_indexer(y_first)[:, :i].sum(axis=1)
Expand Down