From a793bde0571709597d7434c4bdbf6e3ad5cd183b Mon Sep 17 00:00:00 2001 From: Jody Klymak Date: Fri, 3 Apr 2020 16:33:19 -0700 Subject: [PATCH] ENH: add rcParam for ConciseDate and interval_multiples --- doc/users/next_whats_new/rcparams_dates.rst | 31 +++++++++++ lib/matplotlib/dates.py | 58 ++++++++++++++++++--- lib/matplotlib/rcsetup.py | 24 +++++++++ lib/matplotlib/tests/test_dates.py | 38 ++++++++++++++ matplotlibrc.template | 5 +- 5 files changed, 147 insertions(+), 9 deletions(-) create mode 100644 doc/users/next_whats_new/rcparams_dates.rst diff --git a/doc/users/next_whats_new/rcparams_dates.rst b/doc/users/next_whats_new/rcparams_dates.rst new file mode 100644 index 000000000000..370271a99e6b --- /dev/null +++ b/doc/users/next_whats_new/rcparams_dates.rst @@ -0,0 +1,31 @@ +New rcParams for dates: set converter and whether to use interval_multiples +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The new :rc:`date.converter` allows toggling between +`matplotlib.dates.DateConverter` and `matplotlib.dates.ConciseDateConverter` +using the strings 'auto' and 'concise' respectively. + +The new :rc:`date.interval_multiples` allows toggling between the dates +locator trying to pick ticks at set intervals (i.e. day 1 and 15 of the +month), versus evenly spaced ticks that start where ever the +timeseries starts: + +.. plot:: + :include-source: True + + import matplotlib.pyplot as plt + import numpy as np + + dates = np.arange('2001-01-10', '2001-05-23', dtype='datetime64[D]') + y = np.sin(dates.astype(float) / 10) + fig, axs = plt.subplots(nrows=2, constrained_layout=True) + + plt.rcParams['date.converter'] = 'concise' + plt.rcParams['date.interval_multiples'] = True + ax = axs[0] + ax.plot(dates, y) + + plt.rcParams['date.converter'] = 'auto' + plt.rcParams['date.interval_multiples'] = False + ax = axs[1] + ax.plot(dates, y) diff --git a/lib/matplotlib/dates.py b/lib/matplotlib/dates.py index 864c3c060c5d..df916d44b60c 100644 --- a/lib/matplotlib/dates.py +++ b/lib/matplotlib/dates.py @@ -1834,8 +1834,11 @@ class DateConverter(units.ConversionInterface): The 'unit' tag for such data is None or a tzinfo instance. """ - @staticmethod - def axisinfo(unit, axis): + def __init__(self, *, interval_multiples=True): + self._interval_multiples = interval_multiples + super().__init__() + + def axisinfo(self, unit, axis): """ Return the `~matplotlib.units.AxisInfo` for *unit*. @@ -1844,7 +1847,8 @@ def axisinfo(unit, axis): """ tz = unit - majloc = AutoDateLocator(tz=tz) + majloc = AutoDateLocator(tz=tz, + interval_multiples=self._interval_multiples) majfmt = AutoDateFormatter(majloc, tz=tz) datemin = datetime.date(2000, 1, 1) datemax = datetime.date(2010, 1, 1) @@ -1886,17 +1890,19 @@ class ConciseDateConverter(DateConverter): # docstring inherited def __init__(self, formats=None, zero_formats=None, offset_formats=None, - show_offset=True): + show_offset=True, *, interval_multiples=True): self._formats = formats self._zero_formats = zero_formats self._offset_formats = offset_formats self._show_offset = show_offset + self._interval_multiples = interval_multiples super().__init__() def axisinfo(self, unit, axis): # docstring inherited tz = unit - majloc = AutoDateLocator(tz=tz) + majloc = AutoDateLocator(tz=tz, + interval_multiples=self._interval_multiples) majfmt = ConciseDateFormatter(majloc, tz=tz, formats=self._formats, zero_formats=self._zero_formats, offset_formats=self._offset_formats, @@ -1907,6 +1913,42 @@ def axisinfo(self, unit, axis): default_limits=(datemin, datemax)) -units.registry[np.datetime64] = DateConverter() -units.registry[datetime.date] = DateConverter() -units.registry[datetime.datetime] = DateConverter() +class _rcParam_helper: + """ + This helper class is so that we can set the converter for dates + via the validator for the rcParams `date.converter` and + `date.interval_multiples`. Never instatiated. + """ + + conv_st = 'auto' + int_mult = True + + @classmethod + def set_converter(cls, s): + """Called by validator for rcParams date.converter""" + cls.conv_st = s + cls.register_converters() + + @classmethod + def set_int_mult(cls, b): + """Called by validator for rcParams date.interval_multiples""" + cls.int_mult = b + cls.register_converters() + + @classmethod + def register_converters(cls): + """ + Helper to register the date converters when rcParams `date.converter` + and `date.interval_multiples` are changed. Called by the helpers + above. + """ + if cls.conv_st == 'concise': + converter = ConciseDateConverter + else: + converter = DateConverter + + interval_multiples = cls.int_mult + convert = converter(interval_multiples=interval_multiples) + units.registry[np.datetime64] = convert + units.registry[datetime.date] = convert + units.registry[datetime.datetime] = convert diff --git a/lib/matplotlib/rcsetup.py b/lib/matplotlib/rcsetup.py index 09e30e1a903c..7abde998778a 100644 --- a/lib/matplotlib/rcsetup.py +++ b/lib/matplotlib/rcsetup.py @@ -20,6 +20,7 @@ import operator import os import re +import sys import numpy as np @@ -174,6 +175,23 @@ def validate_bool_maybe_none(b): raise ValueError('Could not convert "%s" to bool' % b) +def _validate_date_converter(s): + s = validate_string(s) + mdates = sys.modules.get("matplotlib.dates") + if mdates: + mdates._rcParam_helper.set_converter(s) + + +def _validate_date_int_mult(s): + if s is None: + return + s = validate_bool(s) + # only do this if dates is already imported... + mdates = sys.modules.get("matplotlib.dates") + if mdates: + mdates._rcParam_helper.set_int_mult(s) + + def _validate_tex_preamble(s): if s is None or s == 'None': cbook.warn_deprecated( @@ -1271,6 +1289,11 @@ def _convert_validator_spec(key, conv): "date.autoformatter.second": validate_string, "date.autoformatter.microsecond": validate_string, + # 'auto', 'concise', 'auto-noninterval' + 'date.converter': _validate_date_converter, + # for auto date locator, choose interval_multiples + 'date.interval_multiples': _validate_date_int_mult, + # legend properties "legend.fancybox": validate_bool, "legend.loc": _ignorecase([ @@ -1278,6 +1301,7 @@ def _convert_validator_spec(key, conv): "upper right", "upper left", "lower left", "lower right", "right", "center left", "center right", "lower center", "upper center", "center"]), + # the number of points in the legend line "legend.numpoints": validate_int, # the number of points in the legend line for scatter diff --git a/lib/matplotlib/tests/test_dates.py b/lib/matplotlib/tests/test_dates.py index 4d65a83024c0..70fa15a17d0f 100644 --- a/lib/matplotlib/tests/test_dates.py +++ b/lib/matplotlib/tests/test_dates.py @@ -958,3 +958,41 @@ def test_warn_notintervals(): mdates.date2num(dates[-1])) with pytest.warns(UserWarning, match="AutoDateLocator was unable") as rec: locs = locator() + + +def test_change_converter(): + plt.rcParams['date.converter'] = 'concise' + dates = np.arange('2020-01-01', '2020-05-01', dtype='datetime64[D]') + fig, ax = plt.subplots() + + ax.plot(dates, np.arange(len(dates))) + fig.canvas.draw() + assert ax.get_xticklabels()[0].get_text() == 'Jan' + assert ax.get_xticklabels()[1].get_text() == '15' + + plt.rcParams['date.converter'] = 'auto' + fig, ax = plt.subplots() + + ax.plot(dates, np.arange(len(dates))) + fig.canvas.draw() + assert ax.get_xticklabels()[0].get_text() == 'Jan 01 2020' + assert ax.get_xticklabels()[1].get_text() == 'Jan 15 2020' + + +def test_change_interval_multiples(): + plt.rcParams['date.interval_multiples'] = False + dates = np.arange('2020-01-10', '2020-05-01', dtype='datetime64[D]') + fig, ax = plt.subplots() + + ax.plot(dates, np.arange(len(dates))) + fig.canvas.draw() + assert ax.get_xticklabels()[0].get_text() == 'Jan 10 2020' + assert ax.get_xticklabels()[1].get_text() == 'Jan 24 2020' + + plt.rcParams['date.interval_multiples'] = 'True' + fig, ax = plt.subplots() + + ax.plot(dates, np.arange(len(dates))) + fig.canvas.draw() + assert ax.get_xticklabels()[0].get_text() == 'Jan 15 2020' + assert ax.get_xticklabels()[1].get_text() == 'Feb 01 2020' diff --git a/matplotlibrc.template b/matplotlibrc.template index 8ed34d9c1852..28b385bfa086 100644 --- a/matplotlibrc.template +++ b/matplotlibrc.template @@ -442,10 +442,13 @@ #date.autoformatter.minute: %d %H:%M #date.autoformatter.second: %H:%M:%S #date.autoformatter.microsecond: %M:%S.%f - ## The reference date for Matplotlib's internal date representation ## See https://matplotlib.org/examples/ticks_and_spines/date_precision_and_epochs.py #date.epoch: 1970-01-01T00:00:00 +## 'auto', 'concise': +#date.converter: auto +## For auto converter whether to use interval_multiples: +#date.interval_multiples: True ## *************************************************************************** ## * TICKS *