diff --git a/lib/matplotlib/dates.py b/lib/matplotlib/dates.py index d00579cb8949..d31efcf0d7f5 100644 --- a/lib/matplotlib/dates.py +++ b/lib/matplotlib/dates.py @@ -96,11 +96,12 @@ <../gallery/ticks_and_spines/date_demo_rrule.html>`_. * :class:`AutoDateLocator`: On autoscale, this class picks the best - :class:`RRuleLocator` to set the view limits and the tick + :class:`DateLocator` (e.g., :class:`RRuleLocator`) + to set the view limits and the tick locations. If called with ``interval_multiples=True`` it will make ticks line up with sensible multiples of the tick intervals. E.g. if the interval is 4 hours, it will pick hours 0, 4, 8, etc as ticks. - This behaviour is not garaunteed by default. + This behaviour is not guaranteed by default. Date formatters --------------- @@ -128,6 +129,7 @@ import functools import warnings +import logging from dateutil.rrule import (rrule, MO, TU, WE, TH, FR, SA, SU, YEARLY, @@ -157,6 +159,9 @@ 'seconds', 'minutes', 'hours', 'weeks') +_log = logging.getLogger(__name__) + + # Make a simple UTC instance so we don't always have to import # pytz. From the python datetime library docs: @@ -286,14 +291,21 @@ def _from_ordinalf(x, tz=None): remainder = float(x) - ix - # Round down to the nearest microsecond. - dt += datetime.timedelta(microseconds=int(remainder * MUSECONDS_PER_DAY)) + # Since the input date `x` float is unable to preserve microsecond + # precision of time representation in non-antique years, the + # resulting datetime is rounded to the nearest multiple of + # `musec_prec`. A value of 20 is appropriate for current dates. + musec_prec = 20 + remainder_musec = int(round(remainder * MUSECONDS_PER_DAY / + float(musec_prec)) * musec_prec) - # Compensate for rounding errors - if dt.microsecond < 10: - dt = dt.replace(microsecond=0) - elif dt.microsecond > 999990: - dt += datetime.timedelta(microseconds=1e6 - dt.microsecond) + # For people trying to plot with full microsecond precision, enable + # an early-year workaround + if x < 30 * 365: + remainder_musec = int(round(remainder * MUSECONDS_PER_DAY)) + + # add hours, minutes, seconds, microseconds + dt += datetime.timedelta(microseconds=remainder_musec) return dt.astimezone(tz) @@ -1296,6 +1308,11 @@ def get_locator(self, dmin, dmax): locator = RRuleLocator(rrule, self.tz) else: locator = MicrosecondLocator(interval, tz=self.tz) + if dmin.year > 20 and interval < 1000: + _log.warn('Plotting microsecond time intervals is not' + ' well supported. Please see the' + ' MicrosecondLocator documentation' + ' for details.') locator.set_axis(self.axis) @@ -1511,7 +1528,21 @@ def __init__(self, bysecond=None, interval=1, tz=None): class MicrosecondLocator(DateLocator): """ - Make ticks on occurances of each microsecond. + Make ticks on regular intervals of one or more microsecond(s). + + .. note:: + + Due to the floating point representation of time in days since + 0001-01-01 UTC (plus 1), plotting data with microsecond time + resolution does not work well with current dates. + + If you want microsecond resolution time plots, it is strongly + recommended to use floating point seconds, not datetime-like + time representation. + + If you really must use datetime.datetime() or similar and still + need microsecond precision, your only chance is to use very + early years; using year 0001 is recommended. """ def __init__(self, interval=1, tz=None): diff --git a/lib/matplotlib/tests/baseline_images/test_dates/DateFormatter_fractionalSeconds.png b/lib/matplotlib/tests/baseline_images/test_dates/DateFormatter_fractionalSeconds.png index 580807742a80..67c50f3eded3 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_dates/DateFormatter_fractionalSeconds.png and b/lib/matplotlib/tests/baseline_images/test_dates/DateFormatter_fractionalSeconds.png differ diff --git a/lib/matplotlib/tests/test_dates.py b/lib/matplotlib/tests/test_dates.py index d7231e61ec47..adc39896a2cb 100644 --- a/lib/matplotlib/tests/test_dates.py +++ b/lib/matplotlib/tests/test_dates.py @@ -355,7 +355,6 @@ def _create_auto_date_locator(date1, date2): ['1990-01-01 00:00:00+00:00', '1990-01-01 00:05:00+00:00', '1990-01-01 00:10:00+00:00', '1990-01-01 00:15:00+00:00', '1990-01-01 00:20:00+00:00'] - ], [datetime.timedelta(seconds=40), ['1990-01-01 00:00:00+00:00', '1990-01-01 00:00:05+00:00', @@ -365,11 +364,11 @@ def _create_auto_date_locator(date1, date2): '1990-01-01 00:00:40+00:00'] ], [datetime.timedelta(microseconds=1500), - ['1989-12-31 23:59:59.999507+00:00', + ['1989-12-31 23:59:59.999500+00:00', '1990-01-01 00:00:00+00:00', - '1990-01-01 00:00:00.000502+00:00', - '1990-01-01 00:00:00.001005+00:00', - '1990-01-01 00:00:00.001508+00:00'] + '1990-01-01 00:00:00.000500+00:00', + '1990-01-01 00:00:00.001000+00:00', + '1990-01-01 00:00:00.001500+00:00'] ], ) @@ -438,11 +437,11 @@ def _create_auto_date_locator(date1, date2): '1997-01-01 00:00:40+00:00'] ], [datetime.timedelta(microseconds=1500), - ['1996-12-31 23:59:59.999507+00:00', + ['1996-12-31 23:59:59.999500+00:00', '1997-01-01 00:00:00+00:00', - '1997-01-01 00:00:00.000502+00:00', - '1997-01-01 00:00:00.001005+00:00', - '1997-01-01 00:00:00.001508+00:00'] + '1997-01-01 00:00:00.000500+00:00', + '1997-01-01 00:00:00.001000+00:00', + '1997-01-01 00:00:00.001500+00:00'] ], )