Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 3590c26

Browse files
authored
Merge pull request #19348 from theOehrly/rrule-year-locator
Make YearLocator a subclass of RRuleLocator
2 parents 427b9b6 + 63bfcba commit 3590c26

File tree

3 files changed

+86
-47
lines changed

3 files changed

+86
-47
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
Removed ``dates.YearLocator.replaced`` attribute
2+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
3+
4+
`.YearLocator` is now a subclass of `.RRuleLocator`, and the attribute
5+
``YearLocator.replaced`` has been removed. For tick locations that
6+
required modifying this, a custom rrule and `.RRuleLocator` can be used instead.

lib/matplotlib/dates.py

Lines changed: 29 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1168,6 +1168,15 @@ def __call__(self):
11681168
return self.tick_values(dmin, dmax)
11691169

11701170
def tick_values(self, vmin, vmax):
1171+
start, stop = self._create_rrule(vmin, vmax)
1172+
dates = self.rule.between(start, stop, True)
1173+
if len(dates) == 0:
1174+
return date2num([vmin, vmax])
1175+
return self.raise_if_exceeds(date2num(dates))
1176+
1177+
def _create_rrule(self, vmin, vmax):
1178+
# set appropriate rrule dtstart and until and return
1179+
# start and end
11711180
delta = relativedelta(vmax, vmin)
11721181

11731182
# We need to cap at the endpoints of valid datetime
@@ -1187,10 +1196,7 @@ def tick_values(self, vmin, vmax):
11871196

11881197
self.rule.set(dtstart=start, until=stop)
11891198

1190-
dates = self.rule.between(vmin, vmax, True)
1191-
if len(dates) == 0:
1192-
return date2num([vmin, vmax])
1193-
return self.raise_if_exceeds(date2num(dates))
1199+
return vmin, vmax
11941200

11951201
def _get_unit(self):
11961202
# docstring inherited
@@ -1454,7 +1460,7 @@ def get_locator(self, dmin, dmax):
14541460
return locator
14551461

14561462

1457-
class YearLocator(DateLocator):
1463+
class YearLocator(RRuleLocator):
14581464
"""
14591465
Make ticks on a given day of each year that is a multiple of base.
14601466
@@ -1471,52 +1477,28 @@ def __init__(self, base=1, month=1, day=1, tz=None):
14711477
Mark years that are multiple of base on a given month and day
14721478
(default jan 1).
14731479
"""
1474-
super().__init__(tz)
1480+
rule = rrulewrapper(YEARLY, interval=base, bymonth=month,
1481+
bymonthday=day, **self.hms0d)
1482+
super().__init__(rule, tz)
14751483
self.base = ticker._Edge_integer(base, 0)
1476-
self.replaced = {'month': month,
1477-
'day': day,
1478-
'hour': 0,
1479-
'minute': 0,
1480-
'second': 0,
1481-
}
1482-
if not hasattr(tz, 'localize'):
1483-
# if tz is pytz, we need to do this w/ the localize fcn,
1484-
# otherwise datetime.replace works fine...
1485-
self.replaced['tzinfo'] = tz
14861484

1487-
def __call__(self):
1488-
# if no data have been set, this will tank with a ValueError
1489-
try:
1490-
dmin, dmax = self.viewlim_to_dt()
1491-
except ValueError:
1492-
return []
1485+
def _create_rrule(self, vmin, vmax):
1486+
# 'start' needs to be a multiple of the interval to create ticks on
1487+
# interval multiples when the tick frequency is YEARLY
1488+
ymin = max(self.base.le(vmin.year) * self.base.step, 1)
1489+
ymax = min(self.base.ge(vmax.year) * self.base.step, 9999)
14931490

1494-
return self.tick_values(dmin, dmax)
1491+
c = self.rule._construct
1492+
replace = {'year': ymin,
1493+
'month': c.get('bymonth', 1),
1494+
'day': c.get('bymonthday', 1),
1495+
'hour': 0, 'minute': 0, 'second': 0}
14951496

1496-
def tick_values(self, vmin, vmax):
1497-
ymin = self.base.le(vmin.year) * self.base.step
1498-
ymax = self.base.ge(vmax.year) * self.base.step
1499-
1500-
vmin = vmin.replace(year=ymin, **self.replaced)
1501-
if hasattr(self.tz, 'localize'):
1502-
# look after pytz
1503-
if not vmin.tzinfo:
1504-
vmin = self.tz.localize(vmin, is_dst=True)
1505-
1506-
ticks = [vmin]
1507-
1508-
while True:
1509-
dt = ticks[-1]
1510-
if dt.year >= ymax:
1511-
return date2num(ticks)
1512-
year = dt.year + self.base.step
1513-
dt = dt.replace(year=year, **self.replaced)
1514-
if hasattr(self.tz, 'localize'):
1515-
# look after pytz
1516-
if not dt.tzinfo:
1517-
dt = self.tz.localize(dt, is_dst=True)
1518-
1519-
ticks.append(dt)
1497+
start = vmin.replace(**replace)
1498+
stop = start.replace(year=ymax)
1499+
self.rule.set(dtstart=start, until=stop)
1500+
1501+
return start, stop
15201502

15211503

15221504
class MonthLocator(RRuleLocator):

lib/matplotlib/tests/test_dates.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,18 @@ def test_RRuleLocator_dayrange():
197197
# On success, no overflow error shall be thrown
198198

199199

200+
def test_RRuleLocator_close_minmax():
201+
# if d1 and d2 are very close together, rrule cannot create
202+
# reasonable tick intervals; ensure that this is handled properly
203+
rrule = mdates.rrulewrapper(dateutil.rrule.SECONDLY, interval=5)
204+
loc = mdates.RRuleLocator(rrule)
205+
d1 = datetime.datetime(year=2020, month=1, day=1)
206+
d2 = datetime.datetime(year=2020, month=1, day=1, microsecond=1)
207+
expected = ['2020-01-01 00:00:00+00:00',
208+
'2020-01-01 00:00:00.000001+00:00']
209+
assert list(map(str, mdates.num2date(loc.tick_values(d1, d2)))) == expected
210+
211+
200212
@image_comparison(['DateFormatter_fractionalSeconds.png'])
201213
def test_DateFormatter():
202214
import matplotlib.testing.jpl_units as units
@@ -964,6 +976,45 @@ def test_yearlocator_pytz():
964976
assert st == expected
965977

966978

979+
def test_YearLocator():
980+
def _create_year_locator(date1, date2, **kwargs):
981+
locator = mdates.YearLocator(**kwargs)
982+
locator.create_dummy_axis()
983+
locator.axis.set_view_interval(mdates.date2num(date1),
984+
mdates.date2num(date2))
985+
return locator
986+
987+
d1 = datetime.datetime(1990, 1, 1)
988+
results = ([datetime.timedelta(weeks=52 * 200),
989+
{'base': 20, 'month': 1, 'day': 1},
990+
['1980-01-01 00:00:00+00:00', '2000-01-01 00:00:00+00:00',
991+
'2020-01-01 00:00:00+00:00', '2040-01-01 00:00:00+00:00',
992+
'2060-01-01 00:00:00+00:00', '2080-01-01 00:00:00+00:00',
993+
'2100-01-01 00:00:00+00:00', '2120-01-01 00:00:00+00:00',
994+
'2140-01-01 00:00:00+00:00', '2160-01-01 00:00:00+00:00',
995+
'2180-01-01 00:00:00+00:00', '2200-01-01 00:00:00+00:00']
996+
],
997+
[datetime.timedelta(weeks=52 * 200),
998+
{'base': 20, 'month': 5, 'day': 16},
999+
['1980-05-16 00:00:00+00:00', '2000-05-16 00:00:00+00:00',
1000+
'2020-05-16 00:00:00+00:00', '2040-05-16 00:00:00+00:00',
1001+
'2060-05-16 00:00:00+00:00', '2080-05-16 00:00:00+00:00',
1002+
'2100-05-16 00:00:00+00:00', '2120-05-16 00:00:00+00:00',
1003+
'2140-05-16 00:00:00+00:00', '2160-05-16 00:00:00+00:00',
1004+
'2180-05-16 00:00:00+00:00', '2200-05-16 00:00:00+00:00']
1005+
],
1006+
[datetime.timedelta(weeks=52 * 5),
1007+
{'base': 20, 'month': 9, 'day': 25},
1008+
['1980-09-25 00:00:00+00:00', '2000-09-25 00:00:00+00:00']
1009+
],
1010+
)
1011+
1012+
for delta, arguments, expected in results:
1013+
d2 = d1 + delta
1014+
locator = _create_year_locator(d1, d2, **arguments)
1015+
assert list(map(str, mdates.num2date(locator()))) == expected
1016+
1017+
9671018
def test_DayLocator():
9681019
with pytest.raises(ValueError):
9691020
mdates.DayLocator(interval=-1)

0 commit comments

Comments
 (0)