@@ -625,6 +625,8 @@ def strftimedelta(td, fmt_str):
625
625
fmt_str : str
626
626
format string
627
627
"""
628
+ # TODO: make as compatible as possible with strftime format strings,
629
+ # remove %day
628
630
# *_t values are not partially consumed by there next larger unit
629
631
# e.g. for timedelta(days=1.5): d=1, h=12, H=36
630
632
s_t = td .total_seconds ()
@@ -726,7 +728,114 @@ def set_tzinfo(self, tz):
726
728
self .tz = _get_tzinfo (tz )
727
729
728
730
729
- class ConciseDateFormatter (ticker .Formatter ):
731
+ class _ConciseTimevalueFormatter (ticker .Formatter ):
732
+
733
+ def __init__ (self , locator , show_offset = True , * , usetex = None ):
734
+ self ._locator = locator
735
+ self .offset_string = ''
736
+ self .show_offset = show_offset
737
+ self ._usetex = mpl ._val_or_rc (usetex , 'text.usetex' )
738
+
739
+ self .formats = list ()
740
+ self .zero_formats = list ()
741
+ self .offset_formats = list ()
742
+
743
+ def __call__ (self , x , pos = None ):
744
+ return NotImplemented
745
+
746
+ def _get_formats (self ):
747
+ return self .formats , self .zero_formats , self .offset_formats
748
+
749
+ def _format_ticks (self , tickvalue , ticktuple ):
750
+ # basic algorithm:
751
+ # 1) only display a part of the date if it changes over the ticks.
752
+ # 2) don't display the smaller part of the date if:
753
+ # it is always the same or if it is the start of the
754
+ # year, month, day etc.
755
+ fmts , zerofmts , offsetfmts = self ._get_formats ()
756
+ # fmts: format for most ticks at this level
757
+ # zerofmts: format beginnings of days, months, years, etc.
758
+ # offsetfmts: offset fmt are for the offset in the upper left of the
759
+ # or lower right of the axis.
760
+ show_offset = self .show_offset
761
+
762
+ # determine the level we will label at:
763
+ # mostly 0: years, 1: months, 2: days,
764
+ # 3: hours, 4: minutes, 5: seconds, 6: microseconds
765
+ for level in range (5 , - 1 , - 1 ):
766
+ unique = np .unique (ticktuple [:, level ])
767
+ if len (unique ) > 1 :
768
+ # if 1 is included in unique, the year is shown in ticks
769
+ if level < 2 and np .any (unique == 1 ):
770
+ show_offset = False
771
+ break
772
+ elif level == 0 :
773
+ # all tickdate are the same, so only micros might be different
774
+ # set to the most precise (6: microseconds doesn't exist...)
775
+ level = 5
776
+
777
+ # level is the basic level we will label at.
778
+ # now loop through and decide the actual ticklabels
779
+ zerovals = [0 , 1 , 1 , 0 , 0 , 0 , 0 ]
780
+ labels = ['' ] * len (ticktuple )
781
+ for nn in range (len (ticktuple )):
782
+ if level < 5 :
783
+ if ticktuple [nn ][level ] == zerovals [level ]:
784
+ fmt = zerofmts [level ]
785
+ else :
786
+ fmt = fmts [level ]
787
+ else :
788
+ # special handling for seconds + microseconds
789
+ if (isinstance (tickvalue [nn ], datetime .timedelta )
790
+ and (tickvalue [nn ].total_seconds () % 60 == 0.0 )):
791
+ fmt = zerofmts [level ]
792
+ elif (isinstance (tickvalue [nn ], datetime .datetime )
793
+ and (tickvalue [nn ].second
794
+ == tickvalue [nn ].microsecond == 0 )):
795
+ fmt = zerofmts [level ]
796
+ else :
797
+ fmt = fmts [level ]
798
+ labels [nn ] = self ._format_string (tickvalue [nn ], fmt )
799
+
800
+ # special handling of seconds and microseconds:
801
+ # strip extra zeros and decimal if possible.
802
+ # this is complicated by two factors. 1) we have some level-4 strings
803
+ # here (i.e. 03:00, '0.50000', '1.000') 2) we would like to have the
804
+ # same number of decimals for each string (i.e. 0.5 and 1.0).
805
+ if level >= 5 :
806
+ trailing_zeros = min (
807
+ (len (s ) - len (s .rstrip ('0' )) for s in labels if '.' in s ),
808
+ default = None )
809
+ if trailing_zeros :
810
+ for nn in range (len (labels )):
811
+ if '.' in labels [nn ]:
812
+ labels [nn ] = labels [nn ][:- trailing_zeros ].rstrip ('.' )
813
+
814
+ if show_offset :
815
+ # set the offset string:
816
+ self .offset_string = self ._format_string (tickvalue [- 1 ],
817
+ offsetfmts [level ])
818
+ if self ._usetex :
819
+ self .offset_string = _wrap_in_tex (self .offset_string )
820
+ else :
821
+ self .offset_string = ''
822
+
823
+ if self ._usetex :
824
+ return [_wrap_in_tex (l ) for l in labels ]
825
+ else :
826
+ return labels
827
+
828
+ def get_offset (self ):
829
+ return self .offset_string
830
+
831
+ def format_data_short (self , value ):
832
+ return NotImplemented
833
+
834
+ def _format_string (self , value , fmt ):
835
+ return NotImplemented
836
+
837
+
838
+ class ConciseDateFormatter (_ConciseTimevalueFormatter ):
730
839
"""
731
840
A `.Formatter` which attempts to figure out the best format to use for the
732
841
date, and to make it as compact as possible, but still be complete. This is
@@ -803,7 +912,7 @@ def __init__(self, locator, tz=None, formats=None, offset_formats=None,
803
912
Autoformat the date labels. The default format is used to form an
804
913
initial string, and then redundant elements are removed.
805
914
"""
806
- self . _locator = locator
915
+ super (). __init__ ( locator , show_offset = show_offset , usetex = usetex )
807
916
self ._tz = tz
808
917
self .defaultfmt = '%Y'
809
918
# there are 6 levels with each level getting a specific format
@@ -851,9 +960,6 @@ def __init__(self, locator, tz=None, formats=None, offset_formats=None,
851
960
'%Y-%b-%d' ,
852
961
'%Y-%b-%d' ,
853
962
'%Y-%b-%d %H:%M' ]
854
- self .offset_string = ''
855
- self .show_offset = show_offset
856
- self ._usetex = mpl ._val_or_rc (usetex , 'text.usetex' )
857
963
858
964
def __call__ (self , x , pos = None ):
859
965
formatter = DateFormatter (self .defaultfmt , self ._tz ,
@@ -862,88 +968,99 @@ def __call__(self, x, pos=None):
862
968
863
969
def format_ticks (self , values ):
864
970
tickdatetime = [num2date (value , tz = self ._tz ) for value in values ]
865
- tickdate = np .array ([tdt .timetuple ()[:6 ] for tdt in tickdatetime ])
971
+ ticktuple = np .array ([tdt .timetuple ()[:6 ] for tdt in tickdatetime ])
972
+ return super ()._format_ticks (tickdatetime , ticktuple )
866
973
867
- # basic algorithm:
868
- # 1) only display a part of the date if it changes over the ticks.
869
- # 2) don't display the smaller part of the date if:
870
- # it is always the same or if it is the start of the
871
- # year, month, day etc.
872
- # fmt for most ticks at this level
873
- fmts = self .formats
874
- # format beginnings of days, months, years, etc.
875
- zerofmts = self .zero_formats
876
- # offset fmt are for the offset in the upper left of the
877
- # or lower right of the axis.
878
- offsetfmts = self .offset_formats
879
- show_offset = self .show_offset
974
+ def format_data_short (self , value ):
975
+ return num2date (value , tz = self ._tz ).strftime ('%Y-%m-%d %H:%M:%S' )
880
976
881
- # determine the level we will label at:
882
- # mostly 0: years, 1: months, 2: days,
883
- # 3: hours, 4: minutes, 5: seconds, 6: microseconds
884
- for level in range (5 , - 1 , - 1 ):
885
- unique = np .unique (tickdate [:, level ])
886
- if len (unique ) > 1 :
887
- # if 1 is included in unique, the year is shown in ticks
888
- if level < 2 and np .any (unique == 1 ):
889
- show_offset = False
890
- break
891
- elif level == 0 :
892
- # all tickdate are the same, so only micros might be different
893
- # set to the most precise (6: microseconds doesn't exist...)
894
- level = 5
977
+ def _format_string (self , value , fmt ):
978
+ return value .strftime (fmt )
895
979
896
- # level is the basic level we will label at.
897
- # now loop through and decide the actual ticklabels
898
- zerovals = [0 , 1 , 1 , 0 , 0 , 0 , 0 ]
899
- labels = ['' ] * len (tickdate )
900
- for nn in range (len (tickdate )):
901
- if level < 5 :
902
- if tickdate [nn ][level ] == zerovals [level ]:
903
- fmt = zerofmts [level ]
904
- else :
905
- fmt = fmts [level ]
906
- else :
907
- # special handling for seconds + microseconds
908
- if (tickdatetime [nn ].second == tickdatetime [nn ].microsecond
909
- == 0 ):
910
- fmt = zerofmts [level ]
911
- else :
912
- fmt = fmts [level ]
913
- labels [nn ] = tickdatetime [nn ].strftime (fmt )
914
980
915
- # special handling of seconds and microseconds:
916
- # strip extra zeros and decimal if possible.
917
- # this is complicated by two factors. 1) we have some level-4 strings
918
- # here (i.e. 03:00, '0.50000', '1.000') 2) we would like to have the
919
- # same number of decimals for each string (i.e. 0.5 and 1.0).
920
- if level >= 5 :
921
- trailing_zeros = min (
922
- (len (s ) - len (s .rstrip ('0' )) for s in labels if '.' in s ),
923
- default = None )
924
- if trailing_zeros :
925
- for nn in range (len (labels )):
926
- if '.' in labels [nn ]:
927
- labels [nn ] = labels [nn ][:- trailing_zeros ].rstrip ('.' )
981
+ class ConciseTimedeltaFormatter (_ConciseTimevalueFormatter ):
982
+ # TODO: add docs
928
983
929
- if show_offset :
930
- # set the offset string:
931
- self .offset_string = tickdatetime [- 1 ].strftime (offsetfmts [level ])
932
- if self ._usetex :
933
- self .offset_string = _wrap_in_tex (self .offset_string )
984
+ def __init__ (self , locator , formats = None , offset_formats = None ,
985
+ zero_formats = None , show_offset = True , * , usetex = None ):
986
+ """
987
+ Autoformat the date labels. The default format is used to form an
988
+ initial string, and then redundant elements are removed.
989
+ """
990
+ super ().__init__ (locator , show_offset = show_offset , usetex = usetex )
991
+ self .defaultfmt = '%{d}D'
992
+ # there are 6 levels with each level getting a specific format
993
+ # 0: mostly years, 1: months, 2: days,
994
+ # 3: hours, 4: minutes, 5: seconds
995
+ # level 0 and 1 are unsupported for timedelta and skipped here
996
+ if formats :
997
+ if len (formats ) != 4 :
998
+ raise ValueError ('formats argument must be a list of '
999
+ '4 format strings (or None)' )
1000
+ self .formats = formats
934
1001
else :
935
- self .offset_string = ''
1002
+ self .formats = ['%{d}D' , # days
1003
+ '%h:%m' , # hours
1004
+ '%h:%m' , # minutes
1005
+ '%s.%ms%us' , # secs
1006
+ ]
1007
+ # fmt for zeros ticks at this level. These are
1008
+ # ticks that should be labeled w/ info the level above.
1009
+ # like 02:02:00 can just be labeled 02:02.
1010
+ if zero_formats
10000
:
1011
+ if len (zero_formats ) != 4 :
1012
+ raise ValueError ('zero_formats argument must be a list of '
1013
+ '4 format strings (or4 None)' )
1014
+ self .zero_formats = zero_formats
1015
+ else :
1016
+ # use the users formats for the zero tick formats
1017
+ self .zero_formats = ['' ] + self .formats [:- 1 ]
936
1018
937
- if self ._usetex :
938
- return [_wrap_in_tex (l ) for l in labels ]
1019
+ if offset_formats :
1020
+ if len (offset_formats ) != 4 :
1021
+ raise ValueError ('offset_formats argument must be a list of '
1022
+ '4 format strings (or None)' )
1023
+ self .offset_formats = offset_formats
939
1024
else :
940
- return labels
1025
+ self .offset_formats = ['' ,
1026
+ '' ,
1027
+ '%{d}D' ,
1028
+ '%{d}D %h:%m' ]
941
1029
942
- def get_offset (self ):
943
- return self .offset_string
1030
+ def __call__ (self , x , pos = None ):
1031
+ formatter = TimedeltaFormatter (self .defaultfmt , usetex = self ._usetex )
1032
+ return formatter (x , pos = pos )
1033
+
1034
+ def _make_timetuple (self , td ):
1035
+ # returns a tuple similar in structure to datetime.timetuple
1036
+ # all values are rounded to integer precision
1037
+ s_t = td .total_seconds ()
1038
+ d , s = divmod (s_t , SEC_PER_DAY )
1039
+ m_t , s = divmod (s , SEC_PER_MIN )
1040
+ h , m = divmod (m_t , MIN_PER_HOUR )
1041
+
1042
+ # year, month not supported for timedelta, therefore zero
1043
+ return 0 , 0 , d , h , m , s , td .microseconds
1044
+
1045
+ def _get_formats (self ):
1046
+ # extend list of format strings by two emtpy (and unused) strings for
1047
+ # year and month (necessary for compatibility with base class)
1048
+ ret = list ()
1049
+ for fmts in (self .formats , self .zero_formats , self .offset_formats ):
1050
+ ret .append (["" , "" , * fmts ])
1051
+ return ret
1052
+
1053
+ def format_ticks (self , values ):
1054
+ ticktimedelta = [num2timedelta (value ) for value in values ]
1055
+ ticktuple = np .array ([self ._make_timetuple (tdt )
1056
+ for tdt in ticktimedelta ])
1057
+ return super ()._format_ticks (ticktimedelta , ticktuple )
944
1058
945
1059
def format_data_short (self , value ):
946
- return num2date (value , tz = self ._tz ).strftime ('%Y-%m-%d %H:%M:%S' )
1060
+ return strftimedelta (num2timedelta (value ), '%{d}D %h:%m:%s' )
1061
+
1062
+ def _format_string (self , value , fmt ):
1063
+ return strftimedelta (value , fmt )
947
1064
948
1065
949
1066
class _AutoTimevalueFormatter (ticker .Formatter ):
0 commit comments