From 2aae6d8edd4ec2df1038ac4d28ff5ebcad768a31 Mon Sep 17 00:00:00 2001 From: Javier Vegas-Regidor Date: Mon, 9 Nov 2020 13:09:49 +0100 Subject: [PATCH 1/2] Fix time axis issue --- mapgenerator/plotting/timeseries.py | 163 +++++++++++++++++++++++----- 1 file changed, 136 insertions(+), 27 deletions(-) diff --git a/mapgenerator/plotting/timeseries.py b/mapgenerator/plotting/timeseries.py index 5fbf7b9..045ced7 100644 --- a/mapgenerator/plotting/timeseries.py +++ b/mapgenerator/plotting/timeseries.py @@ -1,18 +1,19 @@ -from ast import parse import math import os import logging -from matplotlib.dates import date2num, num2date +import re import matplotlib.pyplot as plt from matplotlib.gridspec import GridSpec import matplotlib.dates as mdates -import netCDF4 +import matplotlib.ticker as ticker +import matplotlib.cbook as cbook from mapgenerator.plotting.definitions import MapGenerator logger = logging.getLogger(__name__) + class PlotSeries(MapGenerator): """ Main class for plotting time series """ @@ -39,11 +40,7 @@ class PlotSeries(MapGenerator): if not coord and cube.dim_coords: coord = cube.aux_coords[0] - if coord.units.calendar: - points = date2num(coord.units.num2date(coord.points)) - else: - points = coord.points - + points = coord.points if 'xlabel' not in kwargs: kwargs['xlabel'] = self._get_default_title(cube) self._current_fig = plt.figure(figsize=self.plot_size) @@ -91,22 +88,23 @@ class PlotSeries(MapGenerator): if xlimits == 'tight': ax.set_xlim(left=x.min(), right=x.max()) elif xlimits == 'auto': - ax.set_autoscalex_on(True) + ax.set_autoscalex_on(True) else: ax.set_xlim(left=xlimits[0], right=xlimits[1]) if ylimits: if ylimits == 'tight': ax.set_ylim(left=y.min(), right=y.max()) elif ylimits == 'auto': - ax.set_autoscaley_on(True) + ax.set_autoscaley_on(True) else: ax.set_ylim(left=ylimits[0], right=ylimits[1]) ax.grid(b=True, which='major', axis='both', alpha=0.6) ax.grid(b=True, which='minor', axis='both', alpha=0.3) - ax.tick_params(axis='both', which='major', labelsize=self.axis_fontsize) + ax.tick_params( + axis='both', which='major', labelsize=self.axis_fontsize) - - def multiplot_cube(self, cube, coord, multi_coord, ncols=2, invert=False, **kwargs): + def multiplot_cube(self, cube, coord, multi_coord, ncols=2, invert=False, + **kwargs): coord = cube.coord(coord) multi_coord = cube.coord(multi_coord) if multi_coord.shape[0] == 1: @@ -122,9 +120,9 @@ class PlotSeries(MapGenerator): ) suptitle = kwargs.pop('suptitle', '') if suptitle: - suptitle = f"{suptitle}\n\n{self._get_default_title(cube)}" + suptitle = f"{suptitle}\n\n{self._get_default_title(cube)}" else: - suptitle = self._get_default_title(cube) + suptitle = self._get_default_title(cube) self._current_fig.suptitle( suptitle, y=1.0, fontsize=self.suptitle_fontsize ) @@ -134,7 +132,8 @@ class PlotSeries(MapGenerator): self._current_fig.add_subplot(gs[i]) elif sharex: if sharey: - self._current_fig.add_subplot(gs[i], sharex=plt.gca(), sharey=plt.gca()) + self._current_fig.add_subplot( + gs[i], sharex=plt.gca(), sharey=plt.gca()) kwargs.pop('invertx', None) kwargs.pop('inverty', None) else: @@ -150,10 +149,7 @@ class PlotSeries(MapGenerator): title, fontsize=self.title_fontsize ) - if coord.units.calendar: - points = date2num(coord.units.num2date(coord.points)) - else: - points = coord.points + points = coord.points if invert: x = plot_cube.data y = points @@ -175,10 +171,9 @@ class PlotSeries(MapGenerator): self._close() def _save_fig(self, name): - fullname = os.path.join(self.outdir ,f"{name}.{self.filefmt}") - self._current_fig.savefig(fullname, bbox_inches='tight', pad_inches=.2, dpi=self.dpi) - - + fullname = os.path.join(self.outdir, f"{name}.{self.filefmt}") + self._current_fig.savefig( + fullname, bbox_inches='tight', pad_inches=.2, dpi=self.dpi) @staticmethod def _set_time_axis(coord): @@ -198,9 +193,123 @@ class PlotSeries(MapGenerator): else: major_locator = 20 minor_locator = 5 - axis.set_major_locator(mdates.YearLocator(major_locator)) + axis.set_major_locator(YearLocator(coord.units, major_locator)) if minor_locator: - axis.set_minor_locator(mdates.YearLocator(minor_locator)) - axis.set_major_formatter(mdates.DateFormatter('%Y')) + axis.set_minor_locator(YearLocator(coord.units, minor_locator)) + axis.set_major_formatter(DateFormatter('%Y', coord.units)) axis.label._text = f"{coord.name()} (years)" + +class YearLocator(mdates.DateLocator): + """ + Make ticks on a given day of each year that is a multiple of base. + + Examples:: + + # Tick every year on Jan 1st + locator = YearLocator() + + # Tick every 5 years on July 4th + locator = YearLocator(5, month=7, day=4) + """ + def __init__(self, units, base=1, month=1, day=1, tz=None, ): + """ + Mark years that are multiple of base on a given month and day + (default jan 1). + """ + mdates.DateLocator.__init__(self, tz) + self.units = units + self.base = ticker._Edge_integer(base, 0) + self.replaced = {'month': month, + 'day': day, + 'hour': 0, + 'minute': 0, + 'second': 0, + } + + def __call__(self): + # if no data have been set, this will tank with a ValueError + try: + dmin, dmax = self.viewlim_to_dt() + except ValueError: + return [] + + return self.tick_values(dmin, dmax) + + def viewlim_to_dt(self): + """ + Converts the view interval to datetime objects. + """ + vmin, vmax = self.axis.get_view_interval() + if vmin > vmax: + vmin, vmax = vmax, vmin + if vmin < 1: + raise ValueError('view limit minimum {} is less than 1 and ' + 'is an invalid Matplotlib date value. This ' + 'often happens if you pass a non-datetime ' + 'value to an axis that has datetime units' + .format(vmin)) + return self.units.num2date(vmin), self.units.num2date(vmax) + + def tick_values(self, vmin, vmax): + ymin = self.base.le(vmin.year) * self.base.step + ymax = self.base.ge(vmax.year) * self.base.step + + vmin = vmin.replace(year=ymin, **self.replaced) + + ticks = [vmin] + + while True: + dt = ticks[-1] + if dt.year >= ymax: + return self.units.date2num(ticks) + year = dt.year + self.base.step + dt = dt.replace(year=year, **self.replaced) + ticks.append(dt) + + @cbook.deprecated("3.2") + def autoscale(self): + """ + Set the view limits to include the data range. + """ + dmin, dmax = self.datalim_to_dt() + + ymin = self.base.le(dmin.year) + ymax = self.base.ge(dmax.year) + vmin = dmin.replace(year=ymin, **self.replaced) + vmin = vmin.astimezone(self.tz) + vmax = dmax.replace(year=ymax, **self.replaced) + vmax = vmax.astimezone(self.tz) + + vmin = self.units.date2num(vmin) + vmax = self.units.date2num(vmax) + return self.nonsingular(vmin, vmax) + + +class DateFormatter(ticker.Formatter): + """ + Format a tick (in days since the epoch) with a + `~datetime.datetime.strftime` format string. + """ + + illegal_s = re.compile(r"((^|[^%])(%%)*%s)") + + def __init__(self, fmt, units): + """ + Parameters + ---------- + fmt : str + `~datetime.datetime.strftime` format string + tz : `tzinfo`, default: :rc:`timezone` + Ticks timezone. + """ + self.units = units + self.fmt = fmt + + def __call__(self, x, pos=0): + if x == 0: + raise ValueError('DateFormatter found a value of x=0, which is ' + 'an illegal date; this usually occurs because ' + 'you have not informed the axis that it is ' + 'plotting dates, e.g., with ax.xaxis_date()') + return self.units.num2date(x).strftime(self.fmt) -- GitLab From 0312381bcb08162f83501a4e177518e87dd95f11 Mon Sep 17 00:00:00 2001 From: Javier Vegas-Regidor Date: Mon, 9 Nov 2020 13:10:56 +0100 Subject: [PATCH 2/2] Bump version --- mapgenerator/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mapgenerator/__init__.py b/mapgenerator/__init__.py index 8afaa8d..9c99450 100644 --- a/mapgenerator/__init__.py +++ b/mapgenerator/__init__.py @@ -1,4 +1,4 @@ """ Map Generator is a toolkit that provides easy to use 2D plotting and evaluation functions for Earth Sciences """ -__version__ = "1.0.0" +__version__ = "1.0.1" -- GitLab