8000 refactor AutoDateFormatter and add AutoTimedeltaFomratter · matplotlib/matplotlib@ff3aaca · GitHub
[go: up one dir, main page]

Skip to content

Commit ff3aaca

Browse files
committed
refactor AutoDateFormat 8000 ter and add AutoTimedeltaFomratter
1 parent 385620b commit ff3aaca

File tree

1 file changed

+213
-42
lines changed

1 file changed

+213
-42
lines changed

lib/matplotlib/dates.py

Lines changed: 213 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,9 @@
176176
import datetime
177177
import functools
178178
import logging
179+
import math
179180
import re
181+
import string
180182

181183
from dateutil.rrule import (rrule, MO, TU, WE, TH, FR, SA, SU, YEARLY,
182184
MONTHLY, WEEKLY, DAILY, HOURLY, MINUTELY,
@@ -633,10 +635,95 @@ def _wrap_in_tex(text):
633635
return ret_text
634636

635637

638+
class _TimedeltaFormatTemplate(string.Template):
639+
# formatting template for datetime-like formatter strings
640+
delimiter = '%'
641+
642+
643+
def strftimedelta(td, fmt_str):
644+
"""
645+
Return a string representing a timedelta, controlled by an explicit
646+
format string.
647+
648+
Arguments
649+
---------
650+
td : datetime.timedelta
651+
fmt_str : str
652+
format string
653+
"""
654+
# *_t values are not partially consumed by there next larger unit
655+
# e.g. for timedelta(days=1.5): d=1, h=12, H=36
656+
s_t = td.total_seconds()
657+
sign = '-' if s_t < 0 else ''
658+
s_t = abs(s_t)
659+
660+
d, s = divmod(s_t, SEC_PER_DAY)
661+
m_t, s = divmod(s, SEC_PER_MIN)
662+
h, m = divmod(m_t, MIN_PER_HOUR)
663+
h_t, _ = divmod(s_t, SEC_PER_HOUR)
664+
665+
us = td.microseconds
666+
ms, us = divmod(us, 1e3)
667+
668+
# create correctly zero padded string for substitution
669+
# last one is a special for correct day(s) plural
670+
values = {'d': int(d),
671+
'H': int(h_t),
672+
'M': int(m_t),
673+
'S': int(s_t),
674+
'h': '{:02d}'.format(int(h)),
675+
'm': '{:02d}'.format(int(m)),
676+
's': '{:02d}'.format(int(s)),
677+
'ms': '{:03d}'.format(int(ms)),
678+
'us': '{:03d}'.format(int(us)),
679+
'day': 'day' if d == 1 else 'days'}
680+
681+
try:
682+
result = _TimedeltaFormatTemplate(fmt_str).substitute(**values)
683+
except KeyError:
684+
raise ValueError(f"Invalid format string '{fmt_str}' for timedelta")
685+
return sign + result
686+
687+
688+
def strftdnum(td_num, fmt_str):
689+
"""
690+
Return a string representing a matplotlib internal float based timedelta,
691+
controlled by an explicit format string.
692+
693+
Arguments
694+
---------
695+
td_num : float
696+
timedelta in matplotlib float representation
697+
fmt_str : str
698+
format string
699+
"""
700+
td = num2timedelta(td_num)
701+
return strftimedelta(td, fmt_str)
702+
703+
636704
## date tick locators and formatters ###
637705

638706

639-
class DateFormatter(ticker.Formatter):
707+
class TimedeltaFormatter(ticker.Formatter):
708+
def __init__(self, fmt, *, usetex=None):
709+
"""
710+
Parameters
711+
----------
712+
fmt : str
713+
`strftimedelta` format string
714+
usetex : bool, default: :rc:`text.usetex`
715+
To enable/disable the use of TeX's math mode for rendering the
716+
results of the formatter.
717+
"""
718+
self.fmt = fmt
719+
self._usetex = mpl._val_or_rc(usetex, 'text.usetex')
720+
721+
def __call__(self, x, pos=0):
722+
result = strftdnum(x, self.fmt)
723+
return _wrap_in_tex(result) if self._usetex else result
724+
725+
726+
class DateFormatter(TimedeltaFormatter):
640727
"""
641728
Format a tick (in days since the epoch) with a
642729
`~datetime.datetime.strftime` format string.
@@ -654,9 +741,8 @@ def __init__(self, fmt, tz=None, *, usetex=None):
654741
To enable/disable the use of TeX's math mode for rendering the
655742
results of the formatter.
656743
"""
744+
super().__init__(fmt, usetex=usetex)
657745
self.tz = _get_tzinfo(tz)
658-
self.fmt = fmt
659-
self._usetex = mpl._val_or_rc(usetex, 'text.usetex')
660746

661747
def __call__(self, x, pos=0):
662748
result = num2date(x, self.tz).strftime(self.fmt)
@@ -886,7 +972,127 @@ def format_data_short(self, value):
886972
return num2date(value, tz=self._tz).strftime('%Y-%m-%d %H:%M:%S')
887973

888974

889-
class AutoDateFormatter(ticker.Formatter):
975+
class _AutoTimevalueFormatter(ticker.Formatter):
976+
# This class cannot be used directly. It needs to be subclassed,
977+
# self.scaled needs to be set and self._get_template_formatter needs to be
978+
# implemented by the child class
979+
980+
# This can be improved by providing some user-level direction on
981+
# how to choose the best format (precedence, etc.).
982+
983+
# Perhaps a 'struct' that has a field for each time-type where a
984+
# zero would indicate "don't show" and a number would indicate
985+
# "show" with some sort of priority. Same priorities could mean
986+
# show all with the same priority.
987+
988+
# Or more simply, perhaps just a format string for each
989+
# possibility...
990+
991+
def __init__(self, locator, defaultfmt='', *, usetex=None):
992+
self._locator = locator
993+
self.defaultfmt = defaultfmt
994+
self._usetex = mpl._val_or_rc(usetex, 'text.usetex')
995+
self._formatter = self._get_template_formatter(defaultfmt)
996+
self.scaled = dict()
997+
998+
def _set_locator(self, locator):
999+
self._locator = locator
1000+
1001+
def _get_template_formatter(self, fmt):
1002+
return NotImplemented
1003+
1004+
def __call__(self, x, pos=None):
1005+
try:
1006+
locator_unit_scale = float(self._locator._get_unit())
1007+
except AttributeError:
1008+
locator_unit_scale = 1
1009+
# Pick the first scale which is greater than the locator unit.
1010+
fmt = next((fmt for scale, fmt in sorted(self.scaled.items())
1011+
if scale >= locator_unit_scale),
1012+
self.defaultfmt)
1013+
1014+
if isinstance(fmt, str):
1015+
self._formatter = self._get_template_formatter(fmt)
1016+
result = self._formatter(x, pos)
1017+
elif callable(fmt):
1018+
result = fmt(x, pos)
1019+
else:
1020+
raise TypeError(f'Unexpected type passed to {self!r}.')
1021+
1022+
return result
1023+
1024+
1025+
class AutoTimedeltaFormatter(_AutoTimevalueFormatter):
1026+
"""
1027+
A `.Formatter` which attempts to figure out the best format to use. This
1028+
is most useful when used with the `AutoTimedeltaLocator`.
1029+
1030+
`.AutoTimedeltaFormatter` has a ``.scale`` dictionary that maps tick scales
1031+
(the interval in days between one major tick) to format strings; this
1032+
dictionary defaults to ::
1033+
1034+
self.scaled = {
1035+
DAYS_PER_YEAR: rcParams['date.autoformat.year'],
1036+
DAYS_PER_MONTH: rcParams['date.autoformat.month'],
1037+
1: rcParams['date.autoformat.day'],
1038+
1 / HOURS_PER_DAY: rcParams['date.autoformat.hour'],
1039+
1 / MINUTES_PER_DAY: rcParams['date.autoformat.minute'],
1040+
1 / SEC_PER_DAY: rcParams['date.autoformat.second'],
1041+
1 / MUSECONDS_PER_DAY: rcParams['date.autoformat.microsecond'],
1042+
}
1043+
1044+
The formatter uses the format string corresponding to the lowest key in
1045+
the dictionary that is greater or equal to the current scale. Dictionary
1046+
entries can be customized::
1047+
1048+
locator = AutoTimedeltaLocator()
1049+
formatter = AutoTimedeltaFormatter(locator)
1050+
formatter.scaled[1/(24*60)] = '%m:%s' # only show min and sec
1051+
1052+
Custom callables can also be used instead of format strings. The following
1053+
example shows how to use a custom format function to strip trailing zeros
1054+
from decimal seconds and adds the date to the first ticklabel::
1055+
1056+
def my_format_function(x, pos=None):
1057+
pass # TODO: add example
1058+
1059+
formatter.scaled[1/(24*60)] = my_format_function
1060+
"""
1061+
1062+
def __init__(self, locator, defaultfmt='%d %day %h:%m', *, usetex=None):
1063+
"""
1064+
Autoformat the timedelta labels.
1065+
1066+
Parameters
1067+
----------
1068+
locator : `.ticker.Locator`
1069+
Locator that this axis is using.
1070+
1071+
defaultfmt : str
1072+
The default format to use if none of the values in ``self.scaled``
1073+
are greater than the unit returned by ``locator._get_unit()``.
1074+
1075+
usetex : bool, default: :rc:`text.usetex`
1076+
To enable/disable the use of TeX's math mode for rendering the
1077+
results of the formatter. If any entries in ``self.scaled`` are set
1078+
as functions, then it is up to the customized function to enable or
1079+
disable TeX's math mode itself.
1080+
"""
1081+
super().__init__(locator, defaultfmt=defaultfmt, usetex=usetex)
1082+
self.scaled = {
1083+
1: "%d %day",
1084+
1 / HOURS_PER_DAY: '%d %day, %h:%m',
1085+
1 / MINUTES_PER_DAY: '%d %day, %h:%m',
1086+
1 / SEC_PER_DAY: '%d %day, %h:%m:%s',
1087+
1e3 / MUSECONDS_PER_DAY: '%d %day, %h:%m:%s.%ms',
1088+
1 / MUSECONDS_PER_DAY: '%d %day, %h:%m:%s.%ms%us',
1089+
}
1090+
1091+
def _get_template_formatter(self, fmt):
1092+
return TimedeltaFormatter(fmt, usetex=self._usetex)
1093+
1094+
1095+
class AutoDateFormatter(_AutoTimevalueFormatter):
8901096
"""
8911097
A `.Formatter` which attempts to figure out the best format to use. This
8921098
is most useful when used with the `AutoDateLocator`.
@@ -930,18 +1136,6 @@ def my_format_function(x, pos=None):
9301136
9311137
formatter.scaled[1/(24*60)] = my_format_function
9321138
"""
933-
934-
# This can be improved by providing some user-level direction on
935-
# how to choose the best format (precedence, etc.).
936-
937-
# Perhaps a 'struct' that has a field for each time-type where a
938-
# zero would indicate "don't show" and a number would indicate
939-
# "show" with some sort of priority. Same priorities could mean
940-
# show all with the same priority.
941-
942-
# Or more simply, perhaps just a format string for each
943-
# possibility...
944-
9451139
def __init__(self, locator, tz=None, defaultfmt='%Y-%m-%d', *,
9461140
usetex=None):
9471141
"""
@@ -965,12 +1159,9 @@ def __init__(self, locator, tz=None, defaultfmt='%Y-%m-%d', *,
9651159
as functions, then it is up to the customized function to enable or
9661160
disable TeX's math mode itself.
9671161
"""
968-
self._locator = locator
9691162
self._tz = tz
970-
self.defaultfmt = defaultfmt
971-
self._formatter = DateFormatter(self.defaultfmt, tz)
1163+
super().__init__(locator, defaultfmt=defaultfmt, usetex=usetex)
9721164
rcParams = mpl.rcParams
973-
self._usetex = mpl._val_or_rc(usetex, 'text.usetex')
9741165
self.scaled = {
9751166
DAYS_PER_YEAR: rcParams['date.autoformatter.year'],
9761167
DAYS_PER_MONTH: rcParams['date.autoformatter.month'],
@@ -981,28 +1172,8 @@ def __init__(self, locator, tz=None, defaultfmt='%Y-%m-%d', *,
9811172
1 / MUSECONDS_PER_DAY: rcParams['date.autoformatter.microsecond']
9821173
}
9831174

984-
def _set_locator(self, locator):
985-
self._locator = locator
986-
987-
def __call__(self, x, pos=None):
988-
try:
989-
locator_unit_scale = float(self._locator._get_unit())
990-
except AttributeError:
991-
locator_unit_scale = 1
992-
# Pick the first scale which is greater than the locator unit.
993-
fmt = next((fmt for scale, fmt in sorted(self.scaled.items())
994-
if scale >= locator_unit_scale),
995-
self.defaultfmt)
996-
997-
if isinstance(fmt, str):
998-
self._formatter = DateFormatter(fmt, self._tz, usetex=self._usetex)
999-
result = self._formatter(x, pos)
1000-
elif callable(fmt):
1001-
result = fmt(x, pos)
1002-
else:
1003-
raise TypeError(f'Unexpected type passed to {self!r}.')
1004-
1005-
return result
1175+
def _get_template_formatter(self, fmt):
1176+
return DateFormatter(fmt, tz=self._tz, usetex=self._usetex)
10061177

10071178

10081179
class rrulewrapper:

0 commit comments

Comments
 (0)
0