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 1b86cf07a0b1..6744e668bedc 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. @@ -207,6 +208,152 @@ def _get_rc_timezone(): 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 = 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) + + @property + def year(self): + """year""" + return super().year + self._yearoffset + + @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) + + @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): + dt = _datetimey._datetimey_to_datetime(self) + new = dt.astimezone(tz) + new = self._datetime_to_datetimey(new, self._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) + 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 = t1._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 _to_ordinalf(dt): """ Convert :mod:`datetime` or :mod:`date` to the Gregorian date as UTC float @@ -239,6 +386,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 @@ -279,14 +435,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 @@ -302,8 +460,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) @@ -426,12 +586,15 @@ 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) + elif d.size and type(d[0]) == _datetimey: + return _to_ordinalfy_np_vectorized(d) if not d.size: return d return _to_ordinalf_np_vectorized(d) @@ -1077,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): @@ -1092,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): @@ -1144,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)) @@ -1202,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: @@ -1365,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] @@ -1531,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 c65bff221274..000000000000 Binary files a/lib/matplotlib/tests/baseline_images/test_dates/RRuleLocator_bounds.png and /dev/null differ diff --git a/lib/matplotlib/tests/test_dates.py b/lib/matplotlib/tests/test_dates.py index 8e9cb55f3bbd..edc559e81418 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', @@ -772,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