Skip to content

Commit 8525c20

Browse files
authored
Merge pull request #156 from lukelbd/bar-fix
Fix grouped bar plots with datetime64 x coords
2 parents 901603e + 1955c23 commit 8525c20

File tree

2 files changed

+45
-24
lines changed

2 files changed

+45
-24
lines changed

CHANGELOG.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,7 @@ There are quite a lot of deprecations for this release. Since this is
159159

160160
.. rubric:: Bug fixes
161161

162+
- Fix issue drawing bar plots with datetime *x* axes (:pr:`156`).
162163
- Fix issue where `~proplot.ticker.AutoFormatter` tools were not locale-aware, i.e. use
163164
comma as decimal point sometimes (:commit:`c7636296`).
164165
- Fix issue where `~proplot.ticker.AutoFormatter` nonzero-value correction algorithm was

proplot/wrappers.py

Lines changed: 44 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -262,8 +262,8 @@ def _axis_labels_title(data, axis=None, units=True):
262262

263263
def standardize_1d(self, func, *args, **kwargs):
264264
"""
265-
Interprets positional arguments for the "1d" plotting methods
266-
%(methods)s. This also optionally modifies the x axis label, y axis label,
265+
Interpret positional arguments for the "1d" plotting methods so usage is
266+
consistent. This also optionally modifies the x axis label, y axis label,
267267
title, and axis ticks if a `~xarray.DataArray`, `~pandas.DataFrame`, or
268268
`~pandas.Series` is passed.
269269
@@ -280,6 +280,10 @@ def standardize_1d(self, func, *args, **kwargs):
280280
See also
281281
--------
282282
cycle_changer
283+
284+
Note
285+
----
286+
This function wraps the 1d plotting methods: %(methods)s.
283287
"""
284288
# Sanitize input
285289
# TODO: Add exceptions for methods other than 'hist'?
@@ -311,8 +315,10 @@ def standardize_1d(self, func, *args, **kwargs):
311315
# Auto x coords
312316
y = ys[0] # test the first y input
313317
if x is None:
314-
axis = 1 if (name in ('hist', 'boxplot', 'violinplot') or any(
315-
kwargs.get(s, None) for s in ('means', 'medians'))) else 0
318+
axis = int(
319+
name in ('hist', 'boxplot', 'violinplot')
320+
or any(kwargs.get(s, None) for s in ('means', 'medians'))
321+
)
316322
x, _ = _axis_labels_title(y, axis=axis)
317323
x = _to_array(x)
318324
if x.ndim != 1:
@@ -452,8 +458,8 @@ def _standardize_latlon(x, y):
452458

453459
def standardize_2d(self, func, *args, order='C', globe=False, **kwargs):
454460
"""
455-
Interprets positional arguments for the "2d" plotting methods
456-
%(methods)s. This also optionally modifies the x axis label, y axis label,
461+
Interpret positional arguments for the "2d" plotting methods so usage is
462+
consistent. This also optionally modifies the x axis label, y axis label,
457463
title, and axis ticks if a `~xarray.DataArray`, `~pandas.DataFrame`, or
458464
`~pandas.Series` is passed.
459465
@@ -484,6 +490,10 @@ def standardize_2d(self, func, *args, order='C', globe=False, **kwargs):
484490
See also
485491
--------
486492
cmap_changer
493+
494+
Note
495+
----
496+
This function wraps the 2d plotting methods: %(methods)s.
487497
"""
488498
# Sanitize input
489499
name = func.__name__
@@ -587,10 +597,7 @@ def standardize_2d(self, func, *args, order='C', globe=False, **kwargs):
587597
f'Input arrays must be 2d, instead got shape {Z.shape}.'
588598
)
589599
elif Z.shape[1] == xlen and Z.shape[0] == ylen:
590-
if all(
591-
z.ndim == 1 and z.size > 1
592-
and _is_number(z) for z in (x, y)
593-
):
600+
if all(z.ndim == 1 and z.size > 1 and _is_number(z) for z in (x, y)):
594601
x = edges(x)
595602
y = edges(y)
596603
else:
@@ -610,6 +617,7 @@ def standardize_2d(self, func, *args, order='C', globe=False, **kwargs):
610617
f'Z centers {Z.shape} or '
611618
f'Z borders {tuple(i+1 for i in Z.shape)}.'
612619
)
620+
613621
# Optionally re-order
614622
# TODO: Double check this
615623
if order == 'F':
@@ -632,33 +640,27 @@ def standardize_2d(self, func, *args, order='C', globe=False, **kwargs):
632640
f'Input arrays must be 2d, instead got shape {Z.shape}.'
633641
)
634642
elif Z.shape[1] == xlen - 1 and Z.shape[0] == ylen - 1:
635-
if all(
636-
z.ndim == 1 and z.size > 1
637-
and _is_number(z) for z in (x, y)
638-
):
643+
if all(z.ndim == 1 and z.size > 1 and _is_number(z) for z in (x, y)):
639644
x = (x[1:] + x[:-1]) / 2
640645
y = (y[1:] + y[:-1]) / 2
641646
else:
642647
if (
643648
x.ndim == 2 and x.shape[0] > 1 and x.shape[1] > 1
644649
and _is_number(x)
645650
):
646-
x = 0.25 * (
647-
x[:-1, :-1] + x[:-1, 1:] + x[1:, :-1] + x[1:, 1:]
648-
)
651+
x = 0.25 * (x[:-1, :-1] + x[:-1, 1:] + x[1:, :-1] + x[1:, 1:])
649652
if (
650653
y.ndim == 2 and y.shape[0] > 1 and y.shape[1] > 1
651654
and _is_number(y)
652655
):
653-
y = 0.25 * (
654-
y[:-1, :-1] + y[:-1, 1:] + y[1:, :-1] + y[1:, 1:]
655-
)
656+
y = 0.25 * (y[:-1, :-1] + y[:-1, 1:] + y[1:, :-1] + y[1:, 1:])
656657
elif Z.shape[1] != xlen or Z.shape[0] != ylen:
657658
raise ValueError(
658659
f'Input shapes x {x.shape} and y {y.shape} '
659660
f'must match Z centers {Z.shape} '
660661
f'or Z borders {tuple(i+1 for i in Z.shape)}.'
661662
)
663+
662664
# Optionally re-order
663665
# TODO: Double check this
664666
if order == 'F':
@@ -1819,13 +1821,31 @@ def cycle_changer(
18191821
key = 'edgecolors'
18201822
kw[key] = value
18211823

1822-
# Get x coordinates
1823-
x_col, y_first = x, ys[0] # samples
1824+
# Add x coordinates as pi chart labels by default
18241825
if name in ('pie',):
1825-
kw['labels'] = _not_none(labels, x_col) # TODO: move to pie wrapper?
1826+
kw['labels'] = _not_none(labels, x) # TODO: move to pie wrapper?
1827+
1828+
# Step size for grouped bar plots
1829+
# WARNING: This will fail for non-numeric non-datetime64 singleton
1830+
# datatypes but this is good enough for vast majority of most cases.
1831+
x_test = np.atleast_1d(_to_ndarray(x))
1832+
if len(x_test) >= 2:
1833+
x_step = x_test[1:] - x_test[:-1]
1834+
x_step = np.concatenate((x_step, x_step[-1:]))
1835+
elif x_test.dtype == np.datetime64:
1836+
x_step = np.timedelta64(1, 'D')
1837+
else:
1838+
x_step = np.array(0.5)
1839+
if np.issubdtype(x_test.dtype, np.datetime64):
1840+
x_step = x_step.astype('timedelta64[ns]') # avoid int timedelta truncation
1841+
1842+
# Get x coordinates for bar plot
1843+
x_col, y_first = x, ys[0] # samples
18261844
if name in ('bar',): # adjust
18271845
if not stacked:
1828-
x_col = x + (i - ncols / 2 + 0.5) * width / ncols
1846+
scale = i - 0.5 * (ncols - 1) # offset from true coordinate
1847+
scale = width * scale / ncols
1848+
x_col = x + x_step * scale
18291849
elif stacked and y_first.ndim > 1:
18301850
key = 'x' if barh else 'bottom'
18311851
kw[key] = _to_indexer(y_first)[:, :i].sum(axis=1)

0 commit comments

Comments
 (0)