1414 and calendar differences can cause confusing differences between what
1515 Python and mpl give as the number of days since 0001-01-01 and what other
1616 software and databases yield. For example, the `US Naval Observatory
17- <http://www.usno.navy.mil/USNO/astronomical-applications/data-services/jul-date>`_
17+ <http://www.usno.navy.mil/USNO/astronomical-applications/
18+ data-services/jul-date>`_
1819 uses a calendar that switches from Julian to Gregorian in October, 1582.
1920 Hence, using their calculator, the number of days between 0001-01-01 and
2021 2006-04-01 is 732403, whereas using the Gregorian calendar via the datetime
112113import math
113114import datetime
114115from itertools import izip
116+ import warnings
115117
116- import matplotlib
118+
119+ from dateutil .rrule import (rrule , MO , TU , WE , TH , FR , SA , SU , YEARLY ,
120+ MONTHLY , WEEKLY , DAILY , HOURLY , MINUTELY ,
121+ SECONDLY )
122+ from dateutil .relativedelta import relativedelta
123+ import dateutil .parser
117124import numpy as np
118125
126+
127+ import matplotlib
119128import matplotlib .units as units
120129import matplotlib .cbook as cbook
121130import matplotlib .ticker as ticker
122131
123- from dateutil .rrule import rrule , MO , TU , WE , TH , FR , SA , SU , YEARLY , \
124- MONTHLY , WEEKLY , DAILY , HOURLY , MINUTELY , SECONDLY
125- from dateutil .relativedelta import relativedelta
126- import dateutil .parser
127132
128133__all__ = ('date2num' , 'num2date' , 'drange' , 'epoch2num' ,
129134 'num2epoch' , 'mx2num' , 'DateFormatter' ,
130135 'IndexDateFormatter' , 'AutoDateFormatter' , 'DateLocator' ,
131136 'RRuleLocator' , 'AutoDateLocator' , 'YearLocator' ,
132137 'MonthLocator' , 'WeekdayLocator' ,
133138 'DayLocator' , 'HourLocator' , 'MinuteLocator' ,
134- 'SecondLocator' , 'rrule' , 'MO' , 'TU' , 'WE' , 'TH' , 'FR' ,
135- 'SA' , 'SU' , 'YEARLY' , 'MONTHLY' , 'WEEKLY' , 'DAILY' ,
136- 'HOURLY' , 'MINUTELY' , 'SECONDLY' , 'relativedelta' ,
139+ 'SecondLocator' , 'MicrosecondLocator' ,
140+ 'rrule' , 'MO' , 'TU' , 'WE' , 'TH' , 'FR' , 'SA' , 'SU' ,
141+ 'YEARLY' , 'MONTHLY' , 'WEEKLY' , 'DAILY' ,
142+ 'HOURLY' , 'MINUTELY' , 'SECONDLY' , 'MICROSECONDLY' , 'relativedelta' ,
137143 'seconds' , 'minutes' , 'hours' , 'weeks' )
138144
139145
@@ -162,7 +168,7 @@ def _get_rc_timezone():
162168 import pytz
163169 return pytz .timezone (s )
164170
165-
171+ MICROSECONDLY = SECONDLY + 1
166172HOURS_PER_DAY = 24.
167173MINUTES_PER_DAY = 60. * HOURS_PER_DAY
168174SECONDS_PER_DAY = 60. * MINUTES_PER_DAY
@@ -465,6 +471,7 @@ class AutoDateFormatter(ticker.Formatter):
465471 30. : '%b %Y',
466472 1.0 : '%b %d %Y',
467473 1./24. : '%H:%M:%D',
474+ 1. / (24. * 60.): '%H:%M:%S.%f',
468475 }
469476
470477
@@ -498,17 +505,14 @@ def __init__(self, locator, tz=None, defaultfmt='%Y-%m-%d'):
498505 self ._tz = tz
499506 self .defaultfmt = defaultfmt
500507 self ._formatter = DateFormatter (self .defaultfmt , tz )
501- self .scaled = {
502- 365.0 : '%Y' ,
503- 30. : '%b %Y' ,
504- 1.0 : '%b %d %Y' ,
505- 1. / 24. : '%H:%M:%S' ,
506- }
508+ self .scaled = {365.0 : '%Y' ,
509+ 30. : '%b %Y' ,
510+ 1.0 : '%b %d %Y' ,
511+ 1. / 24. : '%H:%M:%S' ,
512+ 1. / (24. * 60. ): '%H:%M:%S.%f' }
507513
508514 def __call__ (self , x , pos = 0 ):
509-
510515 scale = float (self ._locator ._get_unit ())
511-
512516 fmt = self .defaultfmt
513517
514518 for k in sorted (self .scaled ):
@@ -573,6 +577,11 @@ def _get_interval(self):
573577 return 1
574578
575579 def nonsingular (self , vmin , vmax ):
580+ """
581+ Given the proposed upper and lower extent, adjust the range
582+ if it is too close to being singular (i.e. a range of ~0).
583+
584+ """
576585 unit = self ._get_unit ()
577586 interval = self ._get_interval ()
578587 if abs (vmax - vmin ) < 1e-6 :
@@ -639,6 +648,7 @@ def _get_unit(self):
639648 freq = self .rule ._rrule ._freq
640649 return self .get_unit_generic (freq )
641650
651+ @staticmethod
642652 def get_unit_generic (freq ):
643653 if (freq == YEARLY ):
644654 return 365.0
@@ -657,7 +667,6 @@ def get_unit_generic(freq):
657667 else :
658668 # error
659669 return - 1 # or should this just return '1'?
660- get_unit_generic = staticmethod (get_unit_generic )
661670
662671 def _get_interval (self ):
663672 return self .rule ._rrule ._interval
@@ -704,11 +713,11 @@ def autoscale(self):
704713class AutoDateLocator (DateLocator ):
705714 """
706715 On autoscale, this class picks the best
707- :class:`MultipleDateLocator ` to set the view limits and the tick
716+ :class:`DateLocator ` to set the view limits and the tick
708717 locations.
709718 """
710719 def __init__ (self , tz = None , minticks = 5 , maxticks = None ,
711- interval_multiples = False ):
720+ interval_multiples = False ):
712721 """
713722 *minticks* is the minimum number of ticks desired, which is used to
714723 select the type of ticking (yearly, monthly, etc.).
@@ -719,7 +728,7 @@ def __init__(self, tz=None, minticks=5, maxticks=None,
719728 individual rrule frequency constants (YEARLY, MONTHLY, etc.)
720729 to their own maximum number of ticks. This can be used to keep
721730 the number of ticks appropriate to the format chosen in
722- class:`AutoDateFormatter`. Any frequency not specified in this
731+ : class:`AutoDateFormatter`. Any frequency not specified in this
723732 dictionary is given a default value.
724733
725734 *tz* is a :class:`tzinfo` instance.
@@ -735,12 +744,16 @@ def __init__(self, tz=None, minticks=5, maxticks=None,
735744 multiple allowed for that ticking. The default looks like this::
736745
737746 self.intervald = {
738- YEARLY : [1, 2, 4, 5, 10],
747+ YEARLY : [1, 2, 4, 5, 10, 20, 40, 50, 100, 200, 400, 500,
748+ 1000, 2000, 4000, 5000, 10000],
739749 MONTHLY : [1, 2, 3, 4, 6],
740750 DAILY : [1, 2, 3, 7, 14],
741751 HOURLY : [1, 2, 3, 4, 6, 12],
742752 MINUTELY: [1, 5, 10, 15, 30],
743- SECONDLY: [1, 5, 10, 15, 30]
753+ SECONDLY: [1, 5, 10, 15, 30],
754+ MICROSECONDLY: [1, 2, 5, 10, 20, 50, 100, 200, 500, 1000, 2000,
755+ 5000, 10000, 20000, 50000, 100000, 200000, 500000,
756+ 1000000],
744757 }
745758
746759 The interval is used to specify multiples that are appropriate for
@@ -754,11 +767,12 @@ def __init__(self, tz=None, minticks=5, maxticks=None,
754767 DateLocator .__init__ (self , tz )
755768 self ._locator = YearLocator ()
756769 self ._freq = YEARLY
757- self ._freqs = [YEARLY , MONTHLY , DAILY , HOURLY , MINUTELY , SECONDLY ]
770+ self ._freqs = [YEARLY , MONTHLY , DAILY , HOURLY , MINUTELY ,
771+ SECONDLY , MICROSECONDLY ]
758772 self .minticks = minticks
759773
760- self .maxticks = {YEARLY : 16 , MONTHLY : 12 , DAILY : 11 , HOURLY : 16 ,
761- MINUTELY : 11 , SECONDLY : 11 }
774+ self .maxticks = {YEARLY : 11 , MONTHLY : 12 , DAILY : 11 , HOURLY : 12 ,
775+ MINUTELY : 11 , SECONDLY : 11 , MICROSECONDLY : 8 }
762776 if maxticks is not None :
763777 try :
764778 self .maxticks .update (maxticks )
@@ -767,24 +781,35 @@ def __init__(self, tz=None, minticks=5, maxticks=None,
767781 # number of ticks for every frequency and create a
768782 # dictionary for this
769783 self .maxticks = dict (izip (self ._freqs ,
770- [maxticks ] * len (self ._freqs )))
784+ [maxticks ] * len (self ._freqs )))
771785 self .interval_multiples = interval_multiples
772786 self .intervald = {
773- YEARLY : [1 , 2 , 4 , 5 , 10 ],
774- MONTHLY : [1 , 2 , 3 , 4 , 6 ],
775- DAILY : [1 , 2 , 3 , 7 , 14 ],
776- HOURLY : [1 , 2 , 3 , 4 , 6 , 12 ],
777- MINUTELY : [1 , 5 , 10 , 15 , 30 ],
778- SECONDLY : [1 , 5 , 10 , 15 , 30 ]
779- }
787+ YEARLY : [1 , 2 , 4 , 5 , 10 , 20 , 40 , 50 , 100 , 200 , 400 , 500 ,
788+ 1000 , 2000 , 4000 , 5000 , 10000 ],
789+ MONTHLY : [1 , 2 , 3 , 4 , 6 ],
790+ DAILY : [1 , 2 , 3 , 7 , 14 ],
791+ HOURLY : [1 , 2 , 3 , 4 , 6 , 12 ],
792+ MINUTELY : [1 , 5 , 10 , 15 , 30 ],
793+ SECONDLY : [1 , 5 , 10 , 15 , 30 ],
794+ MICROSECONDLY : [1 , 2 , 5 , 10 , 20 , 50 , 100 , 200 , 500 , 1000 , 2000 ,
795+ 5000 , 10000 , 20000 , 50000 , 100000 , 200000 , 500000 ,
796+ 1000000 ]}
780797 self ._byranges = [None , range (1 , 13 ), range (1 , 32 ), range (0 , 24 ),
781- range (0 , 60 ), range (0 , 60 )]
798+ range (0 , 60 ), range (0 , 60 ), None ]
782799
783800 def __call__ (self ):
784801 'Return the locations of the ticks'
785802 self .refresh ()
786803 return self ._locator ()
787804
805+ def nonsingular (self , vmin , vmax ):
806+ # whatever is thrown at us, we can scale the unit.
807+ # But default nonsingular date plots at an ~4 year period.
808+ if vmin == vmax :
809+ vmin = vmin - 365 * 2
810+ vmax = vmax + 365 * 2
811+ return vmin , vmax
812+
788813 def set_axis (self , axis ):
789814 DateLocator .set_axis (self , axis )
790815 self ._locator .set_axis (axis )
@@ -795,7 +820,10 @@ def refresh(self):
795820 self ._locator = self .get_locator (dmin , dmax )
796821
797822 def _get_unit (self ):
798- return RRuleLocator .get_unit_generic (self ._freq )
823+ if self ._freq in [MICROSECONDLY ]:
824+ return 1. / MUSECONDS_PER_DAY
825+ else :
826+ return RRuleLocator .get_unit_generic (self ._freq )
799827
800828 def autoscale (self ):
801829 'Try to choose the view limits intelligently.'
@@ -805,7 +833,6 @@ def autoscale(self):
805833
806834 def get_locator (self , dmin , dmax ):
807835 'Pick the best locator based on a distance.'
808-
809836 delta = relativedelta (dmax , dmin )
810837
811838 numYears = (delta .years * 1.0 )
@@ -814,12 +841,17 @@ def get_locator(self, dmin, dmax):
814841 numHours = (numDays * 24.0 ) + delta .hours
815842 numMinutes = (numHours * 60.0 ) + delta .minutes
816843 numSeconds = (numMinutes * 60.0 ) + delta .seconds
844+ numMicroseconds = (numSeconds * 1e6 ) + delta .microseconds
817845
818- nums = [numYears , numMonths , numDays , numHours , numMinutes , numSeconds ]
846+ nums = [numYears , numMonths , numDays , numHours , numMinutes ,
847+ numSeconds , numMicroseconds ]
848+
849+ use_rrule_locator = [True ] * 6 + [False ]
819850
820851 # Default setting of bymonth, etc. to pass to rrule
821- # [unused (for year), bymonth, bymonthday, byhour, byminute, bysecond]
822- byranges = [None , 1 , 1 , 0 , 0 , 0 ]
852+ # [unused (for year), bymonth, bymonthday, byhour, byminute,
853+ # bysecond, unused (for microseconds)]
854+ byranges = [None , 1 , 1 , 0 , 0 , 0 , None ]
823855
824856 # Loop over all the frequencies and try to find one that gives at
825857 # least a minticks tick positions. Once this is found, look for
@@ -841,8 +873,13 @@ def get_locator(self, dmin, dmax):
841873 if num <= interval * (self .maxticks [freq ] - 1 ):
842874 break
843875 else :
844- # We went through the whole loop without breaking, default to 1
845- interval = 1
876+ # We went through the whole loop without breaking, default to
877+ # the last interval in the list and raise a warning
878+ warnings .warn ('AutoDateLocator was unable to pick an '
879+ 'appropriate interval for this date range. '
880+ 'It may be necessary to add an interval value '
881+ "to the AutoDateLocator's intervald dictionary."
882+ ' Defaulting to {0}.' .format (interval ))
846883
847884 # Set some parameters as appropriate
848885 self ._freq = freq
@@ -856,22 +893,22 @@ def get_locator(self, dmin, dmax):
856893 # We found what frequency to use
857894 break
858895 else :
859- # We couldn't find a good frequency.
860- # do what?
861- # microseconds as floats, but floats from what reference point?
862- byranges = [ None , 1 , 1 , 0 , 0 , 0 ]
863- interval = 1
864-
865- unused , bymonth , bymonthday , byhour , byminute , bysecond = byranges
866- del unused
867-
868- rrule = rrulewrapper ( self . _freq , interval = interval ,
869- dtstart = dmin , until = dmax ,
870- bymonth = bymonth , bymonthday = bymonthday ,
871- byhour = byhour , byminute = byminute ,
872- bysecond = bysecond )
873-
874- locator = RRuleLocator ( rrule , self . tz )
896+ raise ValueError ( 'No sensible date limit could be found in the '
897+ 'AutoDateLocator.' )
898+
899+ if use_rrule_locator [ i ]:
900+ _ , bymonth , bymonthday , byhour , byminute , bysecond , _ = byranges
901+
902+ rrule = rrulewrapper ( self . _freq , interval = interval ,
903+ dtstart = dmin , until = dmax ,
904+ bymonth = bymonth , bymonthday = bymonthday ,
905+ byhour = byhour , byminute = byminute ,
906+ bysecond = bysecond )
907+
908+ locator = RRuleLocator ( rrule , self . tz )
909+ else :
910+ locator = MicrosecondLocator ( interval , tz = self . tz )
911+
875912 locator .set_axis (self .axis )
876913
877914 locator .set_view_interval (* self .axis .get_view_interval ())
@@ -1051,6 +1088,55 @@ def __init__(self, bysecond=None, interval=1, tz=None):
10511088 RRuleLocator .__init__ (self , rule , tz )
10521089
10531090
1091+ class MicrosecondLocator (DateLocator ):
1092+ """
1093+ Make ticks on occurances of each microsecond.
1094+
1095+ """
1096+ def __init__ (self , interval = 1 , tz = None ):
1097+ """
1098+ *interval* is the interval between each iteration. For
1099+ example, if ``interval=2``, mark every second microsecond.
1100+
1101+ """
1102+ self ._interval = interval
1103+ self ._wrapped_locator = ticker .MultipleLocator (interval )
1104+ self .tz = tz
1105+
1106+ def set_axis (self , axis ):
1107+ self ._wrapped_locator .set_axis (axis )
1108+ return DateLocator .set_axis (self , axis )
1109+
1110+ def set_view_interval (self , vmin , vmax ):
1111+ self ._wrapped_locator .set_view_interval (vmin , vmax )
1112+ return DateLocator .set_view_interval (self , vmin , vmax )
1113+
1114+ def set_data_interval (self , vmin , vmax ):
1115+ self ._wrapped_locator .set_data_interval (vmin , vmax )
1116+ return DateLocator .set_data_interval (self , vmin , vmax )
1117+
1118+ def __call__ (self , * args , ** kwargs ):
1119+ vmin , vmax = self .axis .get_view_interval ()
1120+ vmin *= MUSECONDS_PER_DAY
1121+ vmax *= MUSECONDS_PER_DAY
1122+ ticks = self ._wrapped_locator .tick_values (vmin , vmax )
1123+ ticks = [tick / MUSECONDS_PER_DAY for tick in ticks ]
1124+ return ticks
1125+
1126+ def _get_unit (self ):
1127+ """
1128+ Return how many days a unit of the locator is; used for
1129+ intelligent autoscaling.
1130+ """
1131+ return 1. / MUSECONDS_PER_DAY
1132+
1133+ def _get_interval (self ):
1134+ """
1135+ Return the number of units for each tick.
1136+ """
1137+ return self ._interval
1138+
1139+
10541140def _close_to_dt (d1 , d2 , epsilon = 5 ):
10551141 'Assert that datetimes *d1* and *d2* are within *epsilon* microseconds.'
10561142 delta = d2 - d1
0 commit comments