Skip to content

Commit

Permalink
ENH: add rcParam for ConciseDate and interval_multiples
Browse files Browse the repository at this point in the history
  • Loading branch information
jklymak committed Jul 7, 2020
1 parent 1664420 commit a793bde
Show file tree
Hide file tree
Showing 5 changed files with 147 additions and 9 deletions.
31 changes: 31 additions & 0 deletions doc/users/next_whats_new/rcparams_dates.rst
Original file line number Diff line number Diff line change
@@ -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)
58 changes: 50 additions & 8 deletions lib/matplotlib/dates.py
Original file line number Diff line number Diff line change
Expand Up @@ -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*.
Expand All @@ -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)
Expand Down Expand Up @@ -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,
Expand All @@ -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
24 changes: 24 additions & 0 deletions lib/matplotlib/rcsetup.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import operator
import os
import re
import sys

import numpy as np

Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -1271,13 +1289,19 @@ 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([
"best",
"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
Expand Down
38 changes: 38 additions & 0 deletions lib/matplotlib/tests/test_dates.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'
5 changes: 4 additions & 1 deletion matplotlibrc.template
Original file line number Diff line number Diff line change
Expand Up @@ -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 *
Expand Down

0 comments on commit a793bde

Please sign in to comment.