Skip to content

Commit 24cf375

Browse files
committed
Supported datetimes with microseconds, and those with long time series (>160 years).
1 parent 50508af commit 24cf375

23 files changed

+237
-4632
lines changed

lib/matplotlib/dates.py

+124-42
Original file line numberDiff line numberDiff line change
@@ -112,18 +112,21 @@
112112
import math
113113
import datetime
114114
from itertools import izip
115+
import warnings
115116

116-
import matplotlib
117+
118+
from dateutil.rrule import rrule, MO, TU, WE, TH, FR, SA, SU, YEARLY, \
119+
MONTHLY, WEEKLY, DAILY, HOURLY, MINUTELY, SECONDLY
120+
from dateutil.relativedelta import relativedelta
121+
import dateutil.parser
117122
import numpy as np
118123

124+
125+
import matplotlib
119126
import matplotlib.units as units
120127
import matplotlib.cbook as cbook
121128
import matplotlib.ticker as ticker
122129

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
127130

128131
__all__ = ('date2num', 'num2date', 'drange', 'epoch2num',
129132
'num2epoch', 'mx2num', 'DateFormatter',
@@ -133,7 +136,7 @@
133136
'DayLocator', 'HourLocator', 'MinuteLocator',
134137
'SecondLocator', 'rrule', 'MO', 'TU', 'WE', 'TH', 'FR',
135138
'SA', 'SU', 'YEARLY', 'MONTHLY', 'WEEKLY', 'DAILY',
136-
'HOURLY', 'MINUTELY', 'SECONDLY', 'relativedelta',
139+
'HOURLY', 'MINUTELY', 'SECONDLY', 'MICROSECONDLY', 'relativedelta',
137140
'seconds', 'minutes', 'hours', 'weeks')
138141

139142

@@ -162,7 +165,7 @@ def _get_rc_timezone():
162165
import pytz
163166
return pytz.timezone(s)
164167

165-
168+
MICROSECONDLY = SECONDLY + 1
166169
HOURS_PER_DAY = 24.
167170
MINUTES_PER_DAY = 60. * HOURS_PER_DAY
168171
SECONDS_PER_DAY = 60. * MINUTES_PER_DAY
@@ -465,6 +468,8 @@ class AutoDateFormatter(ticker.Formatter):
465468
30. : '%b %Y',
466469
1.0 : '%b %d %Y',
467470
1./24. : '%H:%M:%D',
471+
1./24. : '%H:%M:%S',
472+
1. / 10 * (24. * 60.): '%H:%M:%S.%f',
468473
}
469474
470475
@@ -503,12 +508,11 @@ def __init__(self, locator, tz=None, defaultfmt='%Y-%m-%d'):
503508
30.: '%b %Y',
504509
1.0: '%b %d %Y',
505510
1. / 24.: '%H:%M:%S',
511+
1. / (24. * 60.): '%H:%M:%S.%f',
506512
}
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):
@@ -639,6 +643,7 @@ def _get_unit(self):
639643
freq = self.rule._rrule._freq
640644
return self.get_unit_generic(freq)
641645

646+
@staticmethod
642647
def get_unit_generic(freq):
643648
if (freq == YEARLY):
644649
return 365.0
@@ -657,7 +662,6 @@ def get_unit_generic(freq):
657662
else:
658663
# error
659664
return -1 # or should this just return '1'?
660-
get_unit_generic = staticmethod(get_unit_generic)
661665

662666
def _get_interval(self):
663667
return self.rule._rrule._interval
@@ -704,7 +708,7 @@ def autoscale(self):
704708
class AutoDateLocator(DateLocator):
705709
"""
706710
On autoscale, this class picks the best
707-
:class:`MultipleDateLocator` to set the view limits and the tick
711+
:class:`RRuleLocator` to set the view limits and the tick
708712
locations.
709713
"""
710714
def __init__(self, tz=None, minticks=5, maxticks=None,
@@ -735,12 +739,16 @@ def __init__(self, tz=None, minticks=5, maxticks=None,
735739
multiple allowed for that ticking. The default looks like this::
736740
737741
self.intervald = {
738-
YEARLY : [1, 2, 4, 5, 10],
742+
YEARLY : [1, 2, 4, 5, 10, 20, 40, 50, 100, 200, 400, 500,
743+
1000, 2000, 4000, 5000, 10000],
739744
MONTHLY : [1, 2, 3, 4, 6],
740745
DAILY : [1, 2, 3, 7, 14],
741746
HOURLY : [1, 2, 3, 4, 6, 12],
742747
MINUTELY: [1, 5, 10, 15, 30],
743-
SECONDLY: [1, 5, 10, 15, 30]
748+
SECONDLY: [1, 5, 10, 15, 30],
749+
MICROSECONDLY: [1, 2, 5, 10, 20, 50, 100, 200, 500, 1000, 2000,
750+
5000, 10000, 20000, 50000, 100000, 200000, 500000,
751+
1000000],
744752
}
745753
746754
The interval is used to specify multiples that are appropriate for
@@ -754,11 +762,12 @@ def __init__(self, tz=None, minticks=5, maxticks=None,
754762
DateLocator.__init__(self, tz)
755763
self._locator = YearLocator()
756764
self._freq = YEARLY
757-
self._freqs = [YEARLY, MONTHLY, DAILY, HOURLY, MINUTELY, SECONDLY]
765+
self._freqs = [YEARLY, MONTHLY, DAILY, HOURLY, MINUTELY,
766+
SECONDLY, MICROSECONDLY]
758767
self.minticks = minticks
759768

760-
self.maxticks = {YEARLY: 16, MONTHLY: 12, DAILY: 11, HOURLY: 16,
761-
MINUTELY: 11, SECONDLY: 11}
769+
self.maxticks = {YEARLY: 11, MONTHLY: 12, DAILY: 11, HOURLY: 12,
770+
MINUTELY: 11, SECONDLY: 11, MICROSECONDLY: 8}
762771
if maxticks is not None:
763772
try:
764773
self.maxticks.update(maxticks)
@@ -770,21 +779,33 @@ def __init__(self, tz=None, minticks=5, maxticks=None,
770779
[maxticks] * len(self._freqs)))
771780
self.interval_multiples = interval_multiples
772781
self.intervald = {
773-
YEARLY: [1, 2, 4, 5, 10],
782+
YEARLY: [1, 2, 4, 5, 10, 20, 40, 50, 100, 200, 400, 500,
783+
1000, 2000, 4000, 5000, 10000],
774784
MONTHLY: [1, 2, 3, 4, 6],
775785
DAILY: [1, 2, 3, 7, 14],
776786
HOURLY: [1, 2, 3, 4, 6, 12],
777787
MINUTELY: [1, 5, 10, 15, 30],
778-
SECONDLY: [1, 5, 10, 15, 30]
788+
SECONDLY: [1, 5, 10, 15, 30],
789+
MICROSECONDLY: [1, 2, 5, 10, 20, 50, 100, 200, 500, 1000, 2000,
790+
5000, 10000, 20000, 50000, 100000, 200000, 500000,
791+
1000000],
779792
}
780793
self._byranges = [None, range(1, 13), range(1, 32), range(0, 24),
781-
range(0, 60), range(0, 60)]
794+
range(0, 60), range(0, 60), None]
782795

783796
def __call__(self):
784797
'Return the locations of the ticks'
785798
self.refresh()
786799
return self._locator()
787800

801+
def nonsingular(self, vmin, vmax):
802+
# whatever is thrown at us, we can scale the unit.
803+
# But default nonsigular date plots at an ~4 year period.
804+
if vmin == vmax:
805+
vmin = vmin - 365 * 2
806+
vmax = vmax + 365 * 2
807+
return vmin, vmax
808+
788809
def set_axis(self, axis):
789810
DateLocator.set_axis(self, axis)
790811
self._locator.set_axis(axis)
@@ -795,7 +816,10 @@ def refresh(self):
795816
self._locator = self.get_locator(dmin, dmax)
796817

797818
def _get_unit(self):
798-
return RRuleLocator.get_unit_generic(self._freq)
819+
if self._freq in [MICROSECONDLY]:
820+
return 1./MUSECONDS_PER_DAY
821+
else:
822+
return RRuleLocator.get_unit_generic(self._freq)
799823

800824
def autoscale(self):
801825
'Try to choose the view limits intelligently.'
@@ -805,7 +829,6 @@ def autoscale(self):
805829

806830
def get_locator(self, dmin, dmax):
807831
'Pick the best locator based on a distance.'
808-
809832
delta = relativedelta(dmax, dmin)
810833

811834
numYears = (delta.years * 1.0)
@@ -814,12 +837,18 @@ def get_locator(self, dmin, dmax):
814837
numHours = (numDays * 24.0) + delta.hours
815838
numMinutes = (numHours * 60.0) + delta.minutes
816839
numSeconds = (numMinutes * 60.0) + delta.seconds
840+
numMicroseconds = (numSeconds * 1e6) + \
841+
delta.microseconds
817842

818-
nums = [numYears, numMonths, numDays, numHours, numMinutes, numSeconds]
843+
nums = [numYears, numMonths, numDays, numHours, numMinutes,
844+
numSeconds, numMicroseconds]
845+
846+
use_rrule_locator = [True] * 6 + [False]
819847

820848
# 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]
849+
# [unused (for year), bymonth, bymonthday, byhour, byminute,
850+
# bysecond, unused (for microseconds)]
851+
byranges = [None, 1, 1, 0, 0, 0, None]
823852

824853
# Loop over all the frequencies and try to find one that gives at
825854
# least a minticks tick positions. Once this is found, look for
@@ -841,8 +870,13 @@ def get_locator(self, dmin, dmax):
841870
if num <= interval * (self.maxticks[freq] - 1):
842871
break
843872
else:
844-
# We went through the whole loop without breaking, default to 1
845-
interval = 1
873+
# We went through the whole loop without breaking, default to
874+
# the last interval in the list and raise a warning
875+
warnings.warn('AutoDateLocator was unable to pick an '
876+
'appropriate interval for this date range. '
877+
'It may be necessary to add an interval value '
878+
"to the AutoDateLocator's intervald dictionary."
879+
' Defaulting to {0}.'.format(interval))
846880

847881
# Set some parameters as appropriate
848882
self._freq = freq
@@ -856,22 +890,22 @@ def get_locator(self, dmin, dmax):
856890
# We found what frequency to use
857891
break
858892
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)
893+
raise ValueError('No sensible date limit could be found in the '
894+
'AutoDateLocator.')
895+
896+
if use_rrule_locator[i]:
897+
_, bymonth, bymonthday, byhour, byminute, bysecond, _ = byranges
898+
899+
rrule = rrulewrapper(self._freq, interval=interval,
900+
dtstart=dmin, until=dmax,
901+
bymonth=bymonth, bymonthday=bymonthday,
902+
byhour=byhour, byminute=byminute,
903+
bysecond=bysecond)
904+
905+
locator = RRuleLocator(rrule, self.tz)
906+
else:
907+
locator = MicrosecondLocator(interval, tz=self.tz)
908+
875909
locator.set_axis(self.axis)
876910

877911
locator.set_view_interval(*self.axis.get_view_interval())
@@ -1051,6 +1085,54 @@ def __init__(self, bysecond=None, interval=1, tz=None):
10511085
RRuleLocator.__init__(self, rule, tz)
10521086

10531087

1088+
class MicrosecondLocator(DateLocator):
1089+
"""
1090+
Make ticks on occurances of each second.
1091+
"""
1092+
def __init__(self, interval=1, tz=None):
1093+
"""
1094+
*interval* is the interval between each iteration. For
1095+
example, if ``interval=2``, mark every second miscrosecond.
1096+
1097+
"""
1098+
self._interval = interval
1099+
self._wrapped_locator = ticker.MultipleLocator(interval)
1100+
self.tz = tz
1101+
1102+
def set_axis(self, axis):
1103+
self._wrapped_locator.set_axis(axis)
1104+
return DateLocator.set_axis(self, axis)
1105+
1106+
def set_view_interval(self, vmin, vmax):
1107+
self._wrapped_locator.set_view_interval(vmin, vmax)
1108+
return DateLocator.set_view_interval(self, vmin, vmax)
1109+
1110+
def set_data_interval(self, vmin, vmax):
1111+
self._wrapped_locator.set_data_interval(vmin, vmax)
1112+
return DateLocator.set_data_interval(self, vmin, vmax)
1113+
1114+
def __call__(self, *args, **kwargs):
1115+
vmin, vmax = self.axis.get_view_interval()
1116+
vmin *= MUSECONDS_PER_DAY
1117+
vmax *= MUSECONDS_PER_DAY
1118+
ticks = self._wrapped_locator.tick_values(vmin, vmax)
1119+
ticks = [tick / MUSECONDS_PER_DAY for tick in ticks]
1120+
return ticks
1121+
1122+
def _get_unit(self):
1123+
"""
1124+
Return how many days a unit of the locator is; used for
1125+
intelligent autoscaling.
1126+
"""
1127+
return 1./MUSECONDS_PER_DAY
1128+
1129+
def _get_interval(self):
1130+
"""
1131+
Return the number of units for each tick.
1132+
"""
1133+
return self._interval
1134+
1135+
10541136
def _close_to_dt(d1, d2, epsilon=5):
10551137
'Assert that datetimes *d1* and *d2* are within *epsilon* microseconds.'
10561138
delta = d2 - d1
Loading

0 commit comments

Comments
 (0)