From 0499e328267f6077a3f9d5ccb6edabfa0d577ed7 Mon Sep 17 00:00:00 2001 From: Luke Davis Date: Sat, 9 May 2020 23:24:28 -0600 Subject: [PATCH 1/3] Fix grouped bar plots with datetime64 x coords --- proplot/wrappers.py | 66 ++++++++++++++++++++++++++++----------------- 1 file changed, 42 insertions(+), 24 deletions(-) diff --git a/proplot/wrappers.py b/proplot/wrappers.py index 192c3aa13..b8aea3907 100644 --- a/proplot/wrappers.py +++ b/proplot/wrappers.py @@ -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. @@ -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'? @@ -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: @@ -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. @@ -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__ @@ -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: @@ -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': @@ -632,10 +640,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 - 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: @@ -643,22 +648,19 @@ def standardize_2d(self, func, *args, order='C', globe=False, **kwargs): 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': @@ -1819,13 +1821,29 @@ 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_step = np.atleast_1d(_to_ndarray(x)) + if len(x_step) >= 2: + x_step = x_step[1:] - x_step[:-1] + x_step = np.concatenate((x_step, x_step[-1:])) + elif x_step.dtype == np.datetime64: + x_step = np.timedelta64(1, 'D') + else: + x_step = 0.5 + + # 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) From 3888174d1a926adf61196df3366e693cbaec6075 Mon Sep 17 00:00:00 2001 From: Luke Davis Date: Sat, 9 May 2020 23:37:28 -0600 Subject: [PATCH 2/3] Suport datetime64 bars with multiple columns --- proplot/wrappers.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/proplot/wrappers.py b/proplot/wrappers.py index b8aea3907..cb6d53aaa 100644 --- a/proplot/wrappers.py +++ b/proplot/wrappers.py @@ -1828,14 +1828,16 @@ def cycle_changer( # 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_step = np.atleast_1d(_to_ndarray(x)) - if len(x_step) >= 2: - x_step = x_step[1:] - x_step[:-1] + 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_step.dtype == np.datetime64: + elif x_test.dtype == np.datetime64: x_step = np.timedelta64(1, 'D') else: - x_step = 0.5 + 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 From 1955c23b66bfcf4849a0ba5a638250698b618fa7 Mon Sep 17 00:00:00 2001 From: Luke Davis Date: Sat, 9 May 2020 23:41:25 -0600 Subject: [PATCH 3/3] Update changelog --- CHANGELOG.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 8d7d738ce..2678d2c98 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -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