From c3dc649d4bbaa88a74ca440ba299bfc748cdb6a2 Mon Sep 17 00:00:00 2001 From: Jody Klymak Date: Tue, 20 Aug 2019 13:17:45 -0700 Subject: [PATCH 1/8] Getting class to work --- lib/matplotlib/dates.py | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/lib/matplotlib/dates.py b/lib/matplotlib/dates.py index 1b86cf07a0b1..2957c04b3a84 100644 --- a/lib/matplotlib/dates.py +++ b/lib/matplotlib/dates.py @@ -189,6 +189,7 @@ def _get_rc_timezone(): MIN_PER_HOUR = 60. SEC_PER_MIN = 60. MONTHS_PER_YEAR = 12. +DAYS_PER_400Y = 146097 DAYS_PER_WEEK = 7. DAYS_PER_MONTH = 30. @@ -206,6 +207,41 @@ def _get_rc_timezone(): MO, TU, WE, TH, FR, SA, SU) WEEKDAYS = (MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY) +class _datetimey(datetime.datetime): + + def __new__(cls, year, *args, **kwargs): + if year < 1 or year > 9999: + yearoffset = int(np.floor(year / 400) * 400) - 2000 + year = year - yearoffset + else: + yearoffset = 0 + new = super().__new__(cls, year, *args, **kwargs) + new._yearoffset = yearoffset + return new + + def strftime(self, fmt): + year0 = self.year + self._yearoffset + if year0 < 0: + fmt = fmt.replace('%Y', f'{year0:05d}') + else: + fmt = fmt.replace('%Y', f'{year0:04d}') + return super().strftime(fmt) + + def __str__(self): + return self.strftime('%Y-%m-%dT%H:%M:%S.%f%z') + + @staticmethod + def _datetime_to_datetimey(new, year_offset): + return _datetimey(new.year + year_offset, new.month, new.day, + new.hour, new.minute, new.second, new.microsecond, + new.tzinfo) + + def astimezone(self, tz=None): + print('self', self) + new = super(_datetimey, self).astimezone(tz) + new = self._datetime_to_datetimey(new, self._yearoffset) + return new + def _to_ordinalf(dt): """ From 024a64a62a2c3fce90daaea289d2f3bf593f834b Mon Sep 17 00:00:00 2001 From: Jody Klymak Date: Fri, 23 Aug 2019 08:47:49 -0700 Subject: [PATCH 2/8] Boo --- lib/matplotlib/dates.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/lib/matplotlib/dates.py b/lib/matplotlib/dates.py index 2957c04b3a84..12c05cabc7dd 100644 --- a/lib/matplotlib/dates.py +++ b/lib/matplotlib/dates.py @@ -210,6 +210,7 @@ def _get_rc_timezone(): class _datetimey(datetime.datetime): def __new__(cls, year, *args, **kwargs): + print('New datetimey') if year < 1 or year > 9999: yearoffset = int(np.floor(year / 400) * 400) - 2000 year = year - yearoffset @@ -217,6 +218,7 @@ def __new__(cls, year, *args, **kwargs): yearoffset = 0 new = super().__new__(cls, year, *args, **kwargs) new._yearoffset = yearoffset + print(new._yearoffset) return new def strftime(self, fmt): @@ -240,8 +242,26 @@ def astimezone(self, tz=None): print('self', self) new = super(_datetimey, self).astimezone(tz) new = self._datetime_to_datetimey(new, self._yearoffset) + print('yoff', new._yearoffset) return new + def replace(self, *args, **kwargs): + year = kwargs.pop('year', None) + if year is not None: + if year < 1 or year > 9999: + yearoffset = int(np.floor(year / 400) * 400) - 2000 + year = year - yearoffset + else: + yearoffset = 0 + kwargs['year'] = year + else: + yearoffset = self._yearoffset + new = super().replace(*args, **kwargs) + return self._datetime_to_datetimey(new, yearoffset) + + +def relativedelta(t1, t2): + def _to_ordinalf(dt): """ From 2c067fb56cbd562f4545bda74d77bbd144d84220 Mon Sep 17 00:00:00 2001 From: Jody Klymak Date: Tue, 27 Aug 2019 17:16:39 -0700 Subject: [PATCH 3/8] ENH: allow negative and large datetimes --- lib/matplotlib/dates.py | 213 ++++++++++++------ .../test_dates/RRuleLocator_bounds.png | Bin 22881 -> 0 bytes lib/matplotlib/tests/test_dates.py | 24 +- 3 files changed, 156 insertions(+), 81 deletions(-) delete mode 100644 lib/matplotlib/tests/baseline_images/test_dates/RRuleLocator_bounds.png diff --git a/lib/matplotlib/dates.py b/lib/matplotlib/dates.py index 12c05cabc7dd..229a0ea3644f 100644 --- a/lib/matplotlib/dates.py +++ b/lib/matplotlib/dates.py @@ -210,7 +210,6 @@ def _get_rc_timezone(): class _datetimey(datetime.datetime): def __new__(cls, year, *args, **kwargs): - print('New datetimey') if year < 1 or year > 9999: yearoffset = int(np.floor(year / 400) * 400) - 2000 year = year - yearoffset @@ -218,19 +217,20 @@ def __new__(cls, year, *args, **kwargs): yearoffset = 0 new = super().__new__(cls, year, *args, **kwargs) new._yearoffset = yearoffset - print(new._yearoffset) return new def strftime(self, fmt): - year0 = self.year + self._yearoffset + year0 = super().year + self._yearoffset if year0 < 0: fmt = fmt.replace('%Y', f'{year0:05d}') else: fmt = fmt.replace('%Y', f'{year0:04d}') return super().strftime(fmt) - def __str__(self): - return self.strftime('%Y-%m-%dT%H:%M:%S.%f%z') + @property + def year(self): + """year """ + return super().year + self._yearoffset @staticmethod def _datetime_to_datetimey(new, year_offset): @@ -238,11 +238,37 @@ def _datetime_to_datetimey(new, year_offset): new.hour, new.minute, new.second, new.microsecond, new.tzinfo) + @staticmethod + def _ddays(d1, d2): + dt1 = _datetimey._datetimey_to_datetime(d1) + dt2 = _datetimey._datetimey_to_datetime(d2) + ddays = dt1 - dt2 + dy = (d1._yearoffset - d2._yearoffset) / 400 * DAYS_PER_400Y + return int(ddays.days + dy) + + @staticmethod + def _datetimey_to_datetime(new): + return datetime.datetime(new.year - new._yearoffset, new.month, new.day, + new.hour, new.minute, new.second, new.microsecond, + new.tzinfo) + + @staticmethod + def _datetimey_to_datetime_samey0(t1, t2): + dt1 = _datetimey._datetimey_to_datetime(t1) + dt2 = _datetimey._datetimey_to_datetime(t2) + dy = (t2._yearoffset - t1._yearoffset) / 400 + if t1._yearoffset < t2._yearoffset: + dt2 = dt2 + dy * datetime.timedelta(days = DAYS_PER_400Y) + else: + dt1 = dt1 + datetime.timedelta(days = dy * DAYS_PER_400Y) + + return dt1, dt2 + + def astimezone(self, tz=None): - print('self', self) - new = super(_datetimey, self).astimezone(tz) + dt = _datetimey._datetimey_to_datetime(self) + new = dt.astimezone(tz) new = self._datetime_to_datetimey(new, self._yearoffset) - print('yoff', new._yearoffset) return new def replace(self, *args, **kwargs): @@ -257,11 +283,75 @@ def replace(self, *args, **kwargs): else: yearoffset = self._yearoffset new = super().replace(*args, **kwargs) - return self._datetime_to_datetimey(new, yearoffset) + new._yearoffset = yearoffset + return new + + def __add__(self, other): + # other is a timedelta, but can be big... + deltay = int(np.floor(other.years / 400) * 400) + newo = other - relativedelta(years=deltay) + datet = _datetimey._datetimey_to_datetime(self) + try: + newdt = datet + newo + except: + newdt = datet + relativedelta(days=DAYS_PER_400Y) + newo + deltay = deltay - 400 + + newdty = _datetimey._datetime_to_datetimey(newdt, self._yearoffset + deltay) + return newdty + + def __sub__(self, other): + if isinstance(other, relativedelta): + return self + -other + return NotImplemented + + def __gt__(self, other): + if self.year > other.year: + return True + if self.year < other.year: + return False + datet = _datetimey._datetimey_to_datetime(self) + dateo = _datetimey._datetimey_to_datetime(self) + return datet > dateo + + def __lt__(self, other): + if self.year > other.year: + return False + if self.year < other.year: + return True + datet = _datetimey._datetimey_to_datetime(self) + dateo = _datetimey._datetimey_to_datetime(self) + return datet < dateo + + def __str__(self): + st0 = super().__str__()[4:] + st0 = f'{self.year:04d}' + st0 + return st0 + def _to_dt64(self): + dt64 = np.datetime64(_datetimey._datetimey_to_datetime(self)) + dt64 = dt64.astype('datetime64[s]') + np.timedelta64(int(self._yearoffset / 400)* 146097, 'D') + return dt64 + + + +def _relativedeltay(t1, t2): + """ + relative delta for exteended _datetimey objects... + """ + # a bit of fanciness to try to adjust things for close dates + # that will have a non-year-locator but wrap a 400y boundary... + _yearoffset1 = t2._yearoffset + if t1._yearoffset - t2._yearoffset == 400: + delta = datetime.timedelta(days=DAYS_PER_400Y) + else: + delta = datetime.timedelta(days=0) + dt1 = _datetimey._datetimey_to_datetime(t1) + delta + dt2 = _datetimey._datetimey_to_datetime(t2) + delta = relativedelta(dt1, dt2) + delta = delta + relativedelta(years=_yearoffset1 - t2._yearoffset) + return delta -def relativedelta(t1, t2): - def _to_ordinalf(dt): """ @@ -295,6 +385,15 @@ def _to_ordinalf(dt): _to_ordinalf_np_vectorized = np.vectorize(_to_ordinalf) +def _to_ordinalfy(dt): + datet = _datetimey._datetimey_to_datetime(dt) + base = _to_ordinalf(datet) + base = base + dt._yearoffset / 400 * DAYS_PER_400Y + return base + +_to_ordinalfy_np_vectorized = np.vectorize(_to_ordinalfy) + + def _dt64_to_ordinalf(d): """ Convert `numpy.datetime64` or an ndarray of those types to Gregorian @@ -335,14 +434,16 @@ def _from_ordinalf(x, tz=None): if tz is None: tz = _get_rc_timezone() - ix, remainder = divmod(x, 1) - ix = int(ix) + i0, remainder = divmod(x, 1) + # remainder is sub-day. i0 is integer days + i0 = int(i0) + year_offset, ix = divmod(i0, DAYS_PER_400Y) + year_offset = year_offset * 400 + if ix < 1: - raise ValueError('Cannot convert {} to a date. This often happens if ' - 'non-datetime values are passed to an axis that ' - 'expects datetime objects.'.format(ix)) + ix = ix + DAYS_PER_400Y + year_offset += 400 dt = datetime.datetime.fromordinal(ix).replace(tzinfo=UTC) - # 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 @@ -358,8 +459,10 @@ def _from_ordinalf(x, tz=None): # add hours, minutes, seconds, microseconds dt += datetime.timedelta(microseconds=remainder_musec) - return dt.astimezone(tz) + dt = _datetimey._datetime_to_datetimey(dt, year_offset) + dt = dt.astimezone(tz) + return dt # a version of _from_ordinalf that can operate on numpy arrays _from_ordinalf_np_vectorized = np.vectorize(_from_ordinalf) @@ -482,14 +585,18 @@ def date2num(d): (isinstance(d, np.ndarray) and np.issubdtype(d.dtype, np.datetime64))): return _dt64_to_ordinalf(d) + elif (isinstance(d, _datetimey)): + return _to_ordinalfy(d) return _to_ordinalf(d) else: d = np.asarray(d) - if np.issubdtype(d.dtype, np.datetime64): - return _dt64_to_ordinalf(d) if not d.size: return d + if np.issubdtype(d.dtype, np.datetime64): + return _dt64_to_ordinalf(d) + elif type(d[0]) == _datetimey: + return _to_ordinalfy_np_vectorized(d) return _to_ordinalf_np_vectorized(d) @@ -1133,12 +1240,6 @@ def datalim_to_dt(self): dmin, dmax = self.axis.get_data_interval() if dmin > dmax: dmin, dmax = dmax, dmin - if dmin < 1: - raise ValueError('datalim minimum {} is less than 1 and ' - 'is an invalid Matplotlib date value. This often ' - 'happens if you pass a non-datetime ' - 'value to an axis that has datetime units' - .format(dmin)) return num2date(dmin, self.tz), num2date(dmax, self.tz) def viewlim_to_dt(self): @@ -1148,12 +1249,6 @@ def viewlim_to_dt(self): vmin, vmax = self.axis.get_view_interval() if vmin > vmax: vmin, vmax = vmax, vmin - if vmin < 1: - raise ValueError('view limit minimum {} is less than 1 and ' - 'is an invalid Matplotlib date value. This ' - 'often happens if you pass a non-datetime ' - 'value to an axis that has datetime units' - .format(vmin)) return num2date(vmin, self.tz), num2date(vmax, self.tz) def _get_unit(self): @@ -1200,23 +1295,15 @@ def __call__(self): return self.tick_values(dmin, dmax) def tick_values(self, vmin, vmax): - delta = relativedelta(vmax, vmin) - - # We need to cap at the endpoints of valid datetime - try: - start = vmin - delta - except (ValueError, OverflowError): - start = _from_ordinalf(1.0) - - try: - stop = vmax + delta - except (ValueError, OverflowError): - # The magic number! - stop = _from_ordinalf(3652059.9999999) - - self.rule.set(dtstart=start, until=stop) - - dates = self.rule.between(vmin, vmax, True) + if not isinstance(vmin, _datetimey): + vmin = _datetimey._datetime_to_datetimey(vmin, 0) + if not isinstance(vmax, _datetimey): + vmax = _datetimey._datetime_to_datetimey(vmax, 0) + vmind, vmaxd = _datetimey._datetimey_to_datetime_samey0(vmin, vmax) + self.rule.set(dtstart=vmind, until=vmaxd) + dates = self.rule.between(vmind, vmaxd, inc=True) + dates = [_datetimey._datetime_to_datetimey(date, vmin._yearoffset) + for date in dates] if len(dates) == 0: return date2num([vmin, vmax]) return self.raise_if_exceeds(date2num(dates)) @@ -1258,7 +1345,7 @@ def autoscale(self): Set the view limits to include the data range. """ dmin, dmax = self.datalim_to_dt() - delta = relativedelta(dmax, dmin) + delta = _relativedeltay(dmax, dmin) # We need to cap at the endpoints of valid datetime try: @@ -1421,25 +1508,24 @@ def autoscale(self): def get_locator(self, dmin, dmax): 'Pick the best locator based on a distance.' - delta = relativedelta(dmax, dmin) - tdelta = dmax - dmin - + ndays = _datetimey._ddays(dmax, dmin) + tdelta = _relativedeltay(dmax, dmin) # take absolute difference if dmin > dmax: - delta = -delta - tdelta = -tdelta + # delta = -delta + tdelta = tdelta - # The following uses a mix of calls to relativedelta and timedelta + # The following uses a mix of calls to _relativedeltay and timedelta # methods because there is incomplete overlap in the functionality of # these similar functions, and it's best to avoid doing our own math # whenever possible. - numYears = float(delta.years) - numMonths = numYears * MONTHS_PER_YEAR + delta.months - numDays = tdelta.days # Avoids estimates of days/month, days/year - numHours = numDays * HOURS_PER_DAY + delta.hours - numMinutes = numHours * MIN_PER_HOUR + delta.minutes - numSeconds = np.floor(tdelta.total_seconds()) - numMicroseconds = np.floor(tdelta.total_seconds() * 1e6) + numYears = float(tdelta.years) + numMonths = numYears * MONTHS_PER_YEAR + tdelta.months + numDays = ndays # Avoids estimates of days/month, days/year + numHours = numDays * HOURS_PER_DAY + tdelta.hours + numMinutes = numHours * MIN_PER_HOUR + tdelta.minutes + numSeconds = numMinutes * 60 + tdelta.seconds + numMicroseconds = numSeconds * 1e6 + tdelta.microseconds nums = [numYears, numMonths, numDays, numHours, numMinutes, numSeconds, numMicroseconds] @@ -1587,7 +1673,6 @@ def tick_values(self, vmin, vmax): # look after pytz if not dt.tzinfo: dt = self.tz.localize(dt, is_dst=True) - ticks.append(dt) @cbook.deprecated("3.2") diff --git a/lib/matplotlib/tests/baseline_images/test_dates/RRuleLocator_bounds.png b/lib/matplotlib/tests/baseline_images/test_dates/RRuleLocator_bounds.png deleted file mode 100644 index c65bff22127480c88a9aa623a9e20705a82fdb22..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 22881 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 diff --git a/lib/matplotlib/tests/test_dates.py b/lib/matplotlib/tests/test_dates.py index 8e9cb55f3bbd..783abb6967a5 100644 --- a/lib/matplotlib/tests/test_dates.py +++ b/lib/matplotlib/tests/test_dates.py @@ -156,10 +156,7 @@ def test_too_many_date_ticks(): fig.savefig('junk.png') -@image_comparison(['RRuleLocator_bounds.png']) def test_RRuleLocator(): - import matplotlib.testing.jpl_units as units - units.register() # This will cause the RRuleLocator to go out of bounds when it tries # to add padding to the limits, so we make sure it caps at the correct @@ -191,8 +188,6 @@ def test_RRuleLocator_dayrange(): @image_comparison(['DateFormatter_fractionalSeconds.png']) def test_DateFormatter(): - import matplotlib.testing.jpl_units as units - units.register() # Lets make sure that DateFormatter will allow us to have tick marks # at intervals of fractional seconds. @@ -205,11 +200,6 @@ def test_DateFormatter(): ax.set_autoscale_on(True) ax.plot([t0, tf], [0.0, 1.0], marker='o') - # rrule = mpldates.rrulewrapper( dateutil.rrule.YEARLY, interval=500 ) - # locator = mpldates.RRuleLocator( rrule ) - # ax.xaxis.set_major_locator( locator ) - # ax.xaxis.set_major_formatter( mpldates.AutoDateFormatter(locator) ) - ax.autoscale_view() fig.autofmt_xdate() @@ -331,15 +321,15 @@ def _create_auto_date_locator(date1, date2): '1990-11-01 00:00:00+00:00', '1990-12-01 00:00:00+00:00'] ], [datetime.timedelta(days=141), - ['1990-01-05 00:00:00+00:00', '1990-01-26 00:00:00+00:00', - '1990-02-16 00:00:00+00:00', '1990-03-09 00:00:00+00:00', - '1990-03-30 00:00:00+00:00', '1990-04-20 00:00:00+00:00', - '1990-05-11 00:00:00+00:00'] + ['1990-01-01 00:00:00+00:00', '1990-01-22 00:00:00+00:00', + '1990-02-12 00:00:00+00:00', '1990-03-05 00:00:00+00:00', + '1990-03-26 00:00:00+00:00', '1990-04-16 00:00:00+00:00', + '1990-05-07 00:00:00+00:00'] ], [datetime.timedelta(days=40), - ['1990-01-03 00:00:00+00:00', '1990-01-10 00:00:00+00:00', - '1990-01-17 00:00:00+00:00', '1990-01-24 00:00:00+00:00', - '1990-01-31 00:00:00+00:00', '1990-02-07 00:00:00+00:00'] + ['1990-01-01 00:00:00+00:00', '1990-01-08 00:00:00+00:00', + '1990-01-15 00:00:00+00:00', '1990-01-22 00:00:00+00:00', + '1990-01-29 00:00:00+00:00', '1990-02-05 00:00:00+00:00'] ], [datetime.timedelta(hours=40), ['1990-01-01 00:00:00+00:00', '1990-01-01 04:00:00+00:00', From 9564e3dbb52e439ccedd01abb84e758250560fa5 Mon Sep 17 00:00:00 2001 From: Jody Klymak Date: Wed, 28 Aug 2019 07:47:58 -0700 Subject: [PATCH 4/8] ENH: allow negative and large datetimes --- lib/matplotlib/axes/_axes.py | 1 - lib/matplotlib/dates.py | 5 ++--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index 40e48fd5512f..4bf8d7f41215 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -4417,7 +4417,6 @@ def scatter(self, x, y, s=None, c=None, marker=None, cmap=None, norm=None, linewidths = rcParams['lines.linewidth'] offsets = np.ma.column_stack([x, y]) - collection = mcoll.PathCollection( (path,), scales, facecolors=colors, diff --git a/lib/matplotlib/dates.py b/lib/matplotlib/dates.py index 229a0ea3644f..d2a2f1ee9a5e 100644 --- a/lib/matplotlib/dates.py +++ b/lib/matplotlib/dates.py @@ -588,15 +588,14 @@ def date2num(d): elif (isinstance(d, _datetimey)): return _to_ordinalfy(d) return _to_ordinalf(d) - else: d = np.asarray(d) - if not d.size: - return d if np.issubdtype(d.dtype, np.datetime64): return _dt64_to_ordinalf(d) elif type(d[0]) == _datetimey: return _to_ordinalfy_np_vectorized(d) + if not d.size: + return d return _to_ordinalf_np_vectorized(d) From 0db2c0e04937ea2447e6f3ecdc67dfd345e9a167 Mon Sep 17 00:00:00 2001 From: Jody Klymak Date: Wed, 28 Aug 2019 08:33:01 -0700 Subject: [PATCH 5/8] ENH: allow negative and large datetimes --- lib/matplotlib/dates.py | 4 +++- lib/matplotlib/tests/test_dates.py | 2 ++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/matplotlib/dates.py b/lib/matplotlib/dates.py index d2a2f1ee9a5e..e423c16c6696 100644 --- a/lib/matplotlib/dates.py +++ b/lib/matplotlib/dates.py @@ -359,6 +359,7 @@ def _to_ordinalf(dt): days, preserving hours, minutes, seconds and microseconds. Return value is a :func:`float`. """ + print('_to_ordinalf') # Convert to UTC tzi = getattr(dt, 'tzinfo', None) if tzi is not None: @@ -378,6 +379,7 @@ def _to_ordinalf(dt): # Append the seconds as a fraction of a day base += (dt - rdt).total_seconds() / SEC_PER_DAY + print('reeeturning ', dt, base) return base @@ -592,7 +594,7 @@ def date2num(d): d = np.asarray(d) if np.issubdtype(d.dtype, np.datetime64): return _dt64_to_ordinalf(d) - elif type(d[0]) == _datetimey: + elif d.size and type(d[0]) == _datetimey: return _to_ordinalfy_np_vectorized(d) if not d.size: return d diff --git a/lib/matplotlib/tests/test_dates.py b/lib/matplotlib/tests/test_dates.py index 783abb6967a5..a6dc12113555 100644 --- a/lib/matplotlib/tests/test_dates.py +++ b/lib/matplotlib/tests/test_dates.py @@ -150,7 +150,9 @@ def test_too_many_date_ticks(): assert len(rec) == 1 assert \ 'Attempting to set identical left == right' in str(rec[0].message) + print('Looking here') ax.plot([], []) + print('Looking here') ax.xaxis.set_major_locator(mdates.DayLocator()) with pytest.raises(RuntimeError): fig.savefig('junk.png') From 1f5163aa141f0e01223c3568ef08e74ccce17c0d Mon Sep 17 00:00:00 2001 From: Jody Klymak Date: Wed, 28 Aug 2019 21:15:33 -0700 Subject: [PATCH 6/8] FIX --- lib/matplotlib/dates.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/matplotlib/dates.py b/lib/matplotlib/dates.py index e423c16c6696..49698ca189ef 100644 --- a/lib/matplotlib/dates.py +++ b/lib/matplotlib/dates.py @@ -341,7 +341,7 @@ def _relativedeltay(t1, t2): """ # a bit of fanciness to try to adjust things for close dates # that will have a non-year-locator but wrap a 400y boundary... - _yearoffset1 = t2._yearoffset + _yearoffset1 = t1._yearoffset if t1._yearoffset - t2._yearoffset == 400: delta = datetime.timedelta(days=DAYS_PER_400Y) else: @@ -359,7 +359,6 @@ def _to_ordinalf(dt): days, preserving hours, minutes, seconds and microseconds. Return value is a :func:`float`. """ - print('_to_ordinalf') # Convert to UTC tzi = getattr(dt, 'tzinfo', None) if tzi is not None: @@ -379,7 +378,6 @@ def _to_ordinalf(dt): # Append the seconds as a fraction of a day base += (dt - rdt).total_seconds() / SEC_PER_DAY - print('reeeturning ', dt, base) return base From 9cc8eab78578632d76686acba5197b776d1923c8 Mon Sep 17 00:00:00 2001 From: Jody Klymak Date: Thu, 26 Sep 2019 21:26:23 -0700 Subject: [PATCH 7/8] FIX: flake8 --- lib/matplotlib/dates.py | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/lib/matplotlib/dates.py b/lib/matplotlib/dates.py index 49698ca189ef..1db6badcc687 100644 --- a/lib/matplotlib/dates.py +++ b/lib/matplotlib/dates.py @@ -207,11 +207,12 @@ def _get_rc_timezone(): MO, TU, WE, TH, FR, SA, SU) WEEKDAYS = (MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY) + class _datetimey(datetime.datetime): def __new__(cls, year, *args, **kwargs): if year < 1 or year > 9999: - yearoffset = int(np.floor(year / 400) * 400) - 2000 + yearoffset = int(np.floor(year / 400) * 400) - 2000 year = year - yearoffset else: yearoffset = 0 @@ -229,7 +230,7 @@ def strftime(self, fmt): @property def year(self): - """year """ + """year""" return super().year + self._yearoffset @staticmethod @@ -248,9 +249,9 @@ def _ddays(d1, d2): @staticmethod def _datetimey_to_datetime(new): - return datetime.datetime(new.year - new._yearoffset, new.month, new.day, - new.hour, new.minute, new.second, new.microsecond, - new.tzinfo) + return datetime.datetime(new.year - new._yearoffset, new.month, + new.day, new.hour, new.minute, new.second, + new.microsecond, new.tzinfo) @staticmethod def _datetimey_to_datetime_samey0(t1, t2): @@ -258,13 +259,12 @@ def _datetimey_to_datetime_samey0(t1, t2): dt2 = _datetimey._datetimey_to_datetime(t2) dy = (t2._yearoffset - t1._yearoffset) / 400 if t1._yearoffset < t2._yearoffset: - dt2 = dt2 + dy * datetime.timedelta(days = DAYS_PER_400Y) + dt2 = dt2 + dy * datetime.timedelta(days=DAYS_PER_400Y) else: - dt1 = dt1 + datetime.timedelta(days = dy * DAYS_PER_400Y) + dt1 = dt1 + datetime.timedelta(days=dy * DAYS_PER_400Y) return dt1, dt2 - def astimezone(self, tz=None): dt = _datetimey._datetimey_to_datetime(self) new = dt.astimezone(tz) @@ -275,7 +275,7 @@ def replace(self, *args, **kwargs): year = kwargs.pop('year', None) if year is not None: if year < 1 or year > 9999: - yearoffset = int(np.floor(year / 400) * 400) - 2000 + yearoffset = int(np.floor(year / 400) * 400) - 2000 year = year - yearoffset else: yearoffset = 0 @@ -297,7 +297,8 @@ def __add__(self, other): newdt = datet + relativedelta(days=DAYS_PER_400Y) + newo deltay = deltay - 400 - newdty = _datetimey._datetime_to_datetimey(newdt, self._yearoffset + deltay) + newdty = _datetimey._datetime_to_datetimey(newdt, self._yearoffset + + deltay) return newdty def __sub__(self, other): @@ -330,11 +331,11 @@ def __str__(self): def _to_dt64(self): dt64 = np.datetime64(_datetimey._datetimey_to_datetime(self)) - dt64 = dt64.astype('datetime64[s]') + np.timedelta64(int(self._yearoffset / 400)* 146097, 'D') + dt64 = dt64.astype('datetime64[s]') + + np.timedelta64(int(self._yearoffset / 400)* 146097, 'D') return dt64 - def _relativedeltay(t1, t2): """ relative delta for exteended _datetimey objects... From 6ef003fbd86e238d551fd10ab4bc210b188abb0e Mon Sep 17 00:00:00 2001 From: Jody Klymak Date: Fri, 27 Sep 2019 06:53:29 -0700 Subject: [PATCH 8/8] Negative datetime --- lib/matplotlib/dates.py | 4 ++-- lib/matplotlib/tests/test_dates.py | 23 +++++++++++++++++++++-- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/lib/matplotlib/dates.py b/lib/matplotlib/dates.py index 1db6badcc687..6744e668bedc 100644 --- a/lib/matplotlib/dates.py +++ b/lib/matplotlib/dates.py @@ -331,8 +331,8 @@ def __str__(self): def _to_dt64(self): dt64 = np.datetime64(_datetimey._datetimey_to_datetime(self)) - dt64 = dt64.astype('datetime64[s]') + - np.timedelta64(int(self._yearoffset / 400)* 146097, 'D') + dt64 = (dt64.astype('datetime64[s]') + + np.timedelta64(int(self._yearoffset / 400)* 146097, 'D')) return dt64 diff --git a/lib/matplotlib/tests/test_dates.py b/lib/matplotlib/tests/test_dates.py index a6dc12113555..edc559e81418 100644 --- a/lib/matplotlib/tests/test_dates.py +++ b/lib/matplotlib/tests/test_dates.py @@ -150,9 +150,7 @@ def test_too_many_date_ticks(): assert len(rec) == 1 assert \ 'Attempting to set identical left == right' in str(rec[0].message) - print('Looking here') ax.plot([], []) - print('Looking here') ax.xaxis.set_major_locator(mdates.DayLocator()) with pytest.raises(RuntimeError): fig.savefig('junk.png') @@ -764,3 +762,24 @@ def test_datetime64_in_list(): dt = [np.datetime64('2000-01-01'), np.datetime64('2001-01-01')] dn = mdates.date2num(dt) np.testing.assert_equal(dn, [730120., 730486.]) + + +def test_negative_dt64(): + # test that negative datetime64 work. + dt = np.arange('-4000-01-01', '20000-01-01', 365000, dtype='datetime64[D]') + y = np.arange(len(dt)) + fig, ax = plt.subplots() + ax.plot(dt, y) + fig.canvas.draw() + ticklabels = [tl.get_text() for tl in ax.get_xticklabels()] + expected = ['-4000', '0000', '4000', '8000', '12000', '16000', '20000'] + assert ticklabels == expected + + dt = np.arange('-4000-01-01', '-2000-01-01', 36500, dtype='datetime64[D]') + y = np.arange(len(dt)) + fig, ax = plt.subplots() + ax.plot(dt, y) + fig.canvas.draw() + ticklabels = [tl.get_text() for tl in ax.get_xticklabels()] + expected = [str(e) for e in np.arange(-4000, -1999, 200)] + assert ticklabels == expected