176
176
import datetime
177
177
import functools
178
178
import logging
179
+ import math
179
180
import re
181
+ import string
180
182
181
183
from dateutil .rrule import (rrule , MO , TU , WE , TH , FR , SA , SU , YEARLY ,
182
184
MONTHLY , WEEKLY , DAILY , HOURLY , MINUTELY ,
@@ -633,10 +635,95 @@ def _wrap_in_tex(text):
633
635
return ret_text
634
636
635
637
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
+
636
704
## date tick locators and formatters ###
637
705
638
706
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 ):
640
727
"""
641
728
Format a tick (in days since the epoch) with a
642
729
`~datetime.datetime.strftime` format string.
@@ -654,9 +741,8 @@ def __init__(self, fmt, tz=None, *, usetex=None):
654
741
To enable/disable the use of TeX's math mode for rendering the
655
742
results of the formatter.
656
743
"""
744
+ super ().__init__ (fmt , usetex = usetex )
657
745
self .tz = _get_tzinfo (tz )
658
- self .fmt = fmt
659
- self ._usetex = mpl ._val_or_rc (usetex , 'text.usetex' )
660
746
661
747
def __call__ (self , x , pos = 0 ):
662
748
result = num2date (x , self .tz ).strftime (self .fmt )
@@ -886,7 +972,127 @@ def format_data_short(self, value):
886
972
return num2date (value , tz = self ._tz ).strftime ('%Y-%m-%d %H:%M:%S' )
887
973
888
974
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 ):
890
1096
"""
891
1097
A `.Formatter` which attempts to figure out the best format to use. This
892
1098
is most useful when used with the `AutoDateLocator`.
@@ -930,18 +1136,6 @@ def my_format_function(x, pos=None):
930
1136
931
1137
formatter.scaled[1/(24*60)] = my_format_function
932
1138
"""
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
-
945
1139
def __init__ (self , locator , tz = None , defaultfmt = '%Y-%m-%d' , * ,
946
1140
usetex = None ):
947
1141
"""
@@ -965,12 +1159,9 @@ def __init__(self, locator, tz=None, defaultfmt='%Y-%m-%d', *,
965
1159
as functions, then it is up to the customized function to enable or
966
1160
disable TeX's math mode itself.
967
1161
"""
968
- self ._locator = locator
969
1162
self ._tz = tz
970
- self .defaultfmt = defaultfmt
971
- self ._formatter = DateFormatter (self .defaultfmt , tz )
1163
+ super ().__init__ (locator , defaultfmt = defaultfmt , usetex = usetex )
972
1164
rcParams = mpl .rcParams
973
- self ._usetex = mpl ._val_or_rc (usetex , 'text.usetex' )
974
1165
self .scaled = {
975
1166
DAYS_PER_YEAR : rcParams ['date.autoformatter.year' ],
976
1167
DAYS_PER_MONTH : rcParams ['date.autoformatter.month' ],
@@ -981,28 +1172,8 @@ def __init__(self, locator, tz=None, defaultfmt='%Y-%m-%d', *,
981
1172
1 / MUSECONDS_PER_DAY : rcParams ['date.autoformatter.microsecond' ]
982
1173
}
983
1174
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 )
1006
1177
1007
1178
1008
1179
class rrulewrapper :
0 commit comments