10000 Merge pull request #1925 from pelson/long_timeseries_plots · matplotlib/matplotlib@f30dc0a · GitHub
[go: up one dir, main page]

Skip to content

Commit f30dc0a

Browse files
committed
Merge pull request #1925 from pelson/long_timeseries_plots
Supported datetimes with microseconds, and those with long time series (>160 years).
2 parents d39f9c0 + 03f8ce1 commit f30dc0a

21 files changed

+252
-7127
lines changed

lib/matplotlib/dates.py

Lines changed: 145 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@
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
@@ -112,28 +113,33 @@
112113
import math
113114
import datetime
114115
from 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
117124
import numpy as np
118125

126+
127+
import matplotlib
119128
import matplotlib.units as units
120129
import matplotlib.cbook as cbook
121130
import 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
166172
HOURS_PER_DAY = 24.
167173
MINUTES_PER_DAY = 60. * HOURS_PER_DAY
168174
SECONDS_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):
704713
class 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_P DA70 ER_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+
10541140
def _close_to_dt(d1, d2, epsilon=5):
10551141
'Assert that datetimes *d1* and *d2* are within *epsilon* microseconds.'
10561142
delta = d2 - d1
Binary file not shown.
Loading

0 commit comments

Comments
 (0)
0