Coverage for /usr/lib/python3/dist-packages/matplotlib/dates.py: 22%

663 statements  

« prev     ^ index     » next       coverage.py v7.4.4, created at 2025-06-14 15:25 +0200

1""" 

2Matplotlib provides sophisticated date plotting capabilities, standing on the 

3shoulders of python :mod:`datetime` and the add-on module dateutil_. 

4 

5By default, Matplotlib uses the units machinery described in 

6`~matplotlib.units` to convert `datetime.datetime`, and `numpy.datetime64` 

7objects when plotted on an x- or y-axis. The user does not 

8need to do anything for dates to be formatted, but dates often have strict 

9formatting needs, so this module provides many axis locators and formatters. 

10A basic example using `numpy.datetime64` is:: 

11 

12 import numpy as np 

13 

14 times = np.arange(np.datetime64('2001-01-02'), 

15 np.datetime64('2002-02-03'), np.timedelta64(75, 'm')) 

16 y = np.random.randn(len(times)) 

17 

18 fig, ax = plt.subplots() 

19 ax.plot(times, y) 

20 

21.. seealso:: 

22 

23 - :doc:`/gallery/text_labels_and_annotations/date` 

24 - :doc:`/gallery/ticks/date_concise_formatter` 

25 - :doc:`/gallery/ticks/date_demo_convert` 

26 

27.. _date-format: 

28 

29Matplotlib date format 

30---------------------- 

31 

32Matplotlib represents dates using floating point numbers specifying the number 

33of days since a default epoch of 1970-01-01 UTC; for example, 

341970-01-01, 06:00 is the floating point number 0.25. The formatters and 

35locators require the use of `datetime.datetime` objects, so only dates between 

36year 0001 and 9999 can be represented. Microsecond precision 

37is achievable for (approximately) 70 years on either side of the epoch, and 

3820 microseconds for the rest of the allowable range of dates (year 0001 to 

399999). The epoch can be changed at import time via `.dates.set_epoch` or 

40:rc:`dates.epoch` to other dates if necessary; see 

41:doc:`/gallery/ticks/date_precision_and_epochs` for a discussion. 

42 

43.. note:: 

44 

45 Before Matplotlib 3.3, the epoch was 0000-12-31 which lost modern 

46 microsecond precision and also made the default axis limit of 0 an invalid 

47 datetime. In 3.3 the epoch was changed as above. To convert old 

48 ordinal floats to the new epoch, users can do:: 

49 

50 new_ordinal = old_ordinal + mdates.date2num(np.datetime64('0000-12-31')) 

51 

52 

53There are a number of helper functions to convert between :mod:`datetime` 

54objects and Matplotlib dates: 

55 

56.. currentmodule:: matplotlib.dates 

57 

58.. autosummary:: 

59 :nosignatures: 

60 

61 datestr2num 

62 date2num 

63 num2date 

64 num2timedelta 

65 drange 

66 set_epoch 

67 get_epoch 

68 

69.. note:: 

70 

71 Like Python's `datetime.datetime`, Matplotlib uses the Gregorian calendar 

72 for all conversions between dates and floating point numbers. This practice 

73 is not universal, and calendar differences can cause confusing 

74 differences between what Python and Matplotlib give as the number of days 

75 since 0001-01-01 and what other software and databases yield. For 

76 example, the US Naval Observatory uses a calendar that switches 

77 from Julian to Gregorian in October, 1582. Hence, using their 

78 calculator, the number of days between 0001-01-01 and 2006-04-01 is 

79 732403, whereas using the Gregorian calendar via the datetime 

80 module we find:: 

81 

82 In [1]: date(2006, 4, 1).toordinal() - date(1, 1, 1).toordinal() 

83 Out[1]: 732401 

84 

85All the Matplotlib date converters, tickers and formatters are timezone aware. 

86If no explicit timezone is provided, :rc:`timezone` is assumed, provided as a 

87string. If you want to use a different timezone, pass the *tz* keyword 

88argument of `num2date` to any date tickers or locators you create. This can 

89be either a `datetime.tzinfo` instance or a string with the timezone name that 

90can be parsed by `~dateutil.tz.gettz`. 

91 

92A wide range of specific and general purpose date tick locators and 

93formatters are provided in this module. See 

94:mod:`matplotlib.ticker` for general information on tick locators 

95and formatters. These are described below. 

96 

97The dateutil_ module provides additional code to handle date ticking, making it 

98easy to place ticks on any kinds of dates. See examples below. 

99 

100.. _dateutil: https://dateutil.readthedocs.io 

101 

102Date tickers 

103------------ 

104 

105Most of the date tickers can locate single or multiple values. For example:: 

106 

107 # import constants for the days of the week 

108 from matplotlib.dates import MO, TU, WE, TH, FR, SA, SU 

109 

110 # tick on Mondays every week 

111 loc = WeekdayLocator(byweekday=MO, tz=tz) 

112 

113 # tick on Mondays and Saturdays 

114 loc = WeekdayLocator(byweekday=(MO, SA)) 

115 

116In addition, most of the constructors take an interval argument:: 

117 

118 # tick on Mondays every second week 

119 loc = WeekdayLocator(byweekday=MO, interval=2) 

120 

121The rrule locator allows completely general date ticking:: 

122 

123 # tick every 5th easter 

124 rule = rrulewrapper(YEARLY, byeaster=1, interval=5) 

125 loc = RRuleLocator(rule) 

126 

127The available date tickers are: 

128 

129* `MicrosecondLocator`: Locate microseconds. 

130 

131* `SecondLocator`: Locate seconds. 

132 

133* `MinuteLocator`: Locate minutes. 

134 

135* `HourLocator`: Locate hours. 

136 

137* `DayLocator`: Locate specified days of the month. 

138 

139* `WeekdayLocator`: Locate days of the week, e.g., MO, TU. 

140 

141* `MonthLocator`: Locate months, e.g., 7 for July. 

142 

143* `YearLocator`: Locate years that are multiples of base. 

144 

145* `RRuleLocator`: Locate using a `rrulewrapper`. 

146 `rrulewrapper` is a simple wrapper around dateutil_'s `dateutil.rrule` 

147 which allow almost arbitrary date tick specifications. 

148 See :doc:`rrule example </gallery/ticks/date_demo_rrule>`. 

149 

150* `AutoDateLocator`: On autoscale, this class picks the best `DateLocator` 

151 (e.g., `RRuleLocator`) to set the view limits and the tick locations. If 

152 called with ``interval_multiples=True`` it will make ticks line up with 

153 sensible multiples of the tick intervals. For example, if the interval is 

154 4 hours, it will pick hours 0, 4, 8, etc. as ticks. This behaviour is not 

155 guaranteed by default. 

156 

157Date formatters 

158--------------- 

159 

160The available date formatters are: 

161 

162* `AutoDateFormatter`: attempts to figure out the best format to use. This is 

163 most useful when used with the `AutoDateLocator`. 

164 

165* `ConciseDateFormatter`: also attempts to figure out the best format to use, 

166 and to make the format as compact as possible while still having complete 

167 date information. This is most useful when used with the `AutoDateLocator`. 

168 

169* `DateFormatter`: use `~datetime.datetime.strftime` format strings. 

170""" 

171 

172import datetime 

173import functools 

174import logging 

175import math 

176import re 

177 

178from dateutil.rrule import (rrule, MO, TU, WE, TH, FR, SA, SU, YEARLY, 

179 MONTHLY, WEEKLY, DAILY, HOURLY, MINUTELY, 

180 SECONDLY) 

181from dateutil.relativedelta import relativedelta 

182import dateutil.parser 

183import dateutil.tz 

184import numpy as np 

185 

186import matplotlib as mpl 

187from matplotlib import _api, cbook, ticker, units 

188 

189__all__ = ('datestr2num', 'date2num', 'num2date', 'num2timedelta', 'drange', 

190 'epoch2num', 'num2epoch', 'set_epoch', 'get_epoch', 'DateFormatter', 

191 'ConciseDateFormatter', 'AutoDateFormatter', 

192 'DateLocator', 'RRuleLocator', 'AutoDateLocator', 'YearLocator', 

193 'MonthLocator', 'WeekdayLocator', 

194 'DayLocator', 'HourLocator', 'MinuteLocator', 

195 'SecondLocator', 'MicrosecondLocator', 

196 'rrule', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA', 'SU', 

197 'YEARLY', 'MONTHLY', 'WEEKLY', 'DAILY', 

198 'HOURLY', 'MINUTELY', 'SECONDLY', 'MICROSECONDLY', 'relativedelta', 

199 'DateConverter', 'ConciseDateConverter', 'rrulewrapper') 

200 

201 

202_log = logging.getLogger(__name__) 

203UTC = datetime.timezone.utc 

204 

205 

206def _get_tzinfo(tz=None): 

207 """ 

208 Generate `~datetime.tzinfo` from a string or return `~datetime.tzinfo`. 

209 If None, retrieve the preferred timezone from the rcParams dictionary. 

210 """ 

211 if tz is None: 

212 tz = mpl.rcParams['timezone'] 

213 if tz == 'UTC': 

214 return UTC 

215 if isinstance(tz, str): 

216 tzinfo = dateutil.tz.gettz(tz) 

217 if tzinfo is None: 

218 raise ValueError(f"{tz} is not a valid timezone as parsed by" 

219 " dateutil.tz.gettz.") 

220 return tzinfo 

221 if isinstance(tz, datetime.tzinfo): 

222 return tz 

223 raise TypeError("tz must be string or tzinfo subclass.") 

224 

225 

226# Time-related constants. 

227EPOCH_OFFSET = float(datetime.datetime(1970, 1, 1).toordinal()) 

228# EPOCH_OFFSET is not used by matplotlib 

229JULIAN_OFFSET = 1721424.5 # Julian date at 0000-12-31 

230# note that the Julian day epoch is achievable w/ 

231# np.datetime64('-4713-11-24T12:00:00'); datetime64 is proleptic 

232# Gregorian and BC has a one-year offset. So 

233# np.datetime64('0000-12-31') - np.datetime64('-4713-11-24T12:00') = 1721424.5 

234# Ref: https://en.wikipedia.org/wiki/Julian_day 

235MICROSECONDLY = SECONDLY + 1 

236HOURS_PER_DAY = 24. 

237MIN_PER_HOUR = 60. 

238SEC_PER_MIN = 60. 

239MONTHS_PER_YEAR = 12. 

240 

241DAYS_PER_WEEK = 7. 

242DAYS_PER_MONTH = 30. 

243DAYS_PER_YEAR = 365.0 

244 

245MINUTES_PER_DAY = MIN_PER_HOUR * HOURS_PER_DAY 

246 

247SEC_PER_HOUR = SEC_PER_MIN * MIN_PER_HOUR 

248SEC_PER_DAY = SEC_PER_HOUR * HOURS_PER_DAY 

249SEC_PER_WEEK = SEC_PER_DAY * DAYS_PER_WEEK 

250 

251MUSECONDS_PER_DAY = 1e6 * SEC_PER_DAY 

252 

253MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY = ( 

254 MO, TU, WE, TH, FR, SA, SU) 

255WEEKDAYS = (MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY) 

256 

257# default epoch: passed to np.datetime64... 

258_epoch = None 

259 

260 

261def _reset_epoch_test_example(): 

262 """ 

263 Reset the Matplotlib date epoch so it can be set again. 

264 

265 Only for use in tests and examples. 

266 """ 

267 global _epoch 

268 _epoch = None 

269 

270 

271def set_epoch(epoch): 

272 """ 

273 Set the epoch (origin for dates) for datetime calculations. 

274 

275 The default epoch is :rc:`dates.epoch` (by default 1970-01-01T00:00). 

276 

277 If microsecond accuracy is desired, the date being plotted needs to be 

278 within approximately 70 years of the epoch. Matplotlib internally 

279 represents dates as days since the epoch, so floating point dynamic 

280 range needs to be within a factor of 2^52. 

281 

282 `~.dates.set_epoch` must be called before any dates are converted 

283 (i.e. near the import section) or a RuntimeError will be raised. 

284 

285 See also :doc:`/gallery/ticks/date_precision_and_epochs`. 

286 

287 Parameters 

288 ---------- 

289 epoch : str 

290 valid UTC date parsable by `numpy.datetime64` (do not include 

291 timezone). 

292 

293 """ 

294 global _epoch 

295 if _epoch is not None: 

296 raise RuntimeError('set_epoch must be called before dates plotted.') 

297 _epoch = epoch 

298 

299 

300def get_epoch(): 

301 """ 

302 Get the epoch used by `.dates`. 

303 

304 Returns 

305 ------- 

306 epoch : str 

307 String for the epoch (parsable by `numpy.datetime64`). 

308 """ 

309 global _epoch 

310 

311 if _epoch is None: 

312 _epoch = mpl.rcParams['date.epoch'] 

313 return _epoch 

314 

315 

316def _dt64_to_ordinalf(d): 

317 """ 

318 Convert `numpy.datetime64` or an ndarray of those types to Gregorian 

319 date as UTC float relative to the epoch (see `.get_epoch`). Roundoff 

320 is float64 precision. Practically: microseconds for dates between 

321 290301 BC, 294241 AD, milliseconds for larger dates 

322 (see `numpy.datetime64`). 

323 """ 

324 

325 # the "extra" ensures that we at least allow the dynamic range out to 

326 # seconds. That should get out to +/-2e11 years. 

327 dseconds = d.astype('datetime64[s]') 

328 extra = (d - dseconds).astype('timedelta64[ns]') 

329 t0 = np.datetime64(get_epoch(), 's') 

330 dt = (dseconds - t0).astype(np.float64) 

331 dt += extra.astype(np.float64) / 1.0e9 

332 dt = dt / SEC_PER_DAY 

333 

334 NaT_int = np.datetime64('NaT').astype(np.int64) 

335 d_int = d.astype(np.int64) 

336 dt[d_int == NaT_int] = np.nan 

337 return dt 

338 

339 

340def _from_ordinalf(x, tz=None): 

341 """ 

342 Convert Gregorian float of the date, preserving hours, minutes, 

343 seconds and microseconds. Return value is a `.datetime`. 

344 

345 The input date *x* is a float in ordinal days at UTC, and the output will 

346 be the specified `.datetime` object corresponding to that time in 

347 timezone *tz*, or if *tz* is ``None``, in the timezone specified in 

348 :rc:`timezone`. 

349 """ 

350 

351 tz = _get_tzinfo(tz) 

352 

353 dt = (np.datetime64(get_epoch()) + 

354 np.timedelta64(int(np.round(x * MUSECONDS_PER_DAY)), 'us')) 

355 if dt < np.datetime64('0001-01-01') or dt >= np.datetime64('10000-01-01'): 

356 raise ValueError(f'Date ordinal {x} converts to {dt} (using ' 

357 f'epoch {get_epoch()}), but Matplotlib dates must be ' 

358 'between year 0001 and 9999.') 

359 # convert from datetime64 to datetime: 

360 dt = dt.tolist() 

361 

362 # datetime64 is always UTC: 

363 dt = dt.replace(tzinfo=dateutil.tz.gettz('UTC')) 

364 # but maybe we are working in a different timezone so move. 

365 dt = dt.astimezone(tz) 

366 # fix round off errors 

367 if np.abs(x) > 70 * 365: 

368 # if x is big, round off to nearest twenty microseconds. 

369 # This avoids floating point roundoff error 

370 ms = round(dt.microsecond / 20) * 20 

371 if ms == 1000000: 

372 dt = dt.replace(microsecond=0) + datetime.timedelta(seconds=1) 

373 else: 

374 dt = dt.replace(microsecond=ms) 

375 

376 return dt 

377 

378 

379# a version of _from_ordinalf that can operate on numpy arrays 

380_from_ordinalf_np_vectorized = np.vectorize(_from_ordinalf, otypes="O") 

381 

382 

383# a version of dateutil.parser.parse that can operate on numpy arrays 

384_dateutil_parser_parse_np_vectorized = np.vectorize(dateutil.parser.parse) 

385 

386 

387def datestr2num(d, default=None): 

388 """ 

389 Convert a date string to a datenum using `dateutil.parser.parse`. 

390 

391 Parameters 

392 ---------- 

393 d : str or sequence of str 

394 The dates to convert. 

395 

396 default : datetime.datetime, optional 

397 The default date to use when fields are missing in *d*. 

398 """ 

399 if isinstance(d, str): 

400 dt = dateutil.parser.parse(d, default=default) 

401 return date2num(dt) 

402 else: 

403 if default is not None: 

404 d = [date2num(dateutil.parser.parse(s, default=default)) 

405 for s in d] 

406 return np.asarray(d) 

407 d = np.asarray(d) 

408 if not d.size: 

409 return d 

410 return date2num(_dateutil_parser_parse_np_vectorized(d)) 

411 

412 

413def date2num(d): 

414 """ 

415 Convert datetime objects to Matplotlib dates. 

416 

417 Parameters 

418 ---------- 

419 d : `datetime.datetime` or `numpy.datetime64` or sequences of these 

420 

421 Returns 

422 ------- 

423 float or sequence of floats 

424 Number of days since the epoch. See `.get_epoch` for the 

425 epoch, which can be changed by :rc:`date.epoch` or `.set_epoch`. If 

426 the epoch is "1970-01-01T00:00:00" (default) then noon Jan 1 1970 

427 ("1970-01-01T12:00:00") returns 0.5. 

428 

429 Notes 

430 ----- 

431 The Gregorian calendar is assumed; this is not universal practice. 

432 For details see the module docstring. 

433 """ 

434 # Unpack in case of e.g. Pandas or xarray object 

435 d = cbook._unpack_to_numpy(d) 

436 

437 # make an iterable, but save state to unpack later: 

438 iterable = np.iterable(d) 

439 if not iterable: 

440 d = [d] 

441 

442 d = np.asarray(d) 

443 # convert to datetime64 arrays, if not already: 

444 if not np.issubdtype(d.dtype, np.datetime64): 

445 # datetime arrays 

446 if not d.size: 

447 # deals with an empty array... 

448 return d 

449 tzi = getattr(d[0], 'tzinfo', None) 

450 if tzi is not None: 

451 # make datetime naive: 

452 d = [dt.astimezone(UTC).replace(tzinfo=None) for dt in d] 

453 d = np.asarray(d) 

454 d = d.astype('datetime64[us]') 

455 

456 d = _dt64_to_ordinalf(d) 

457 

458 return d if iterable else d[0] 

459 

460 

461def julian2num(j): 

462 """ 

463 Convert a Julian date (or sequence) to a Matplotlib date (or sequence). 

464 

465 Parameters 

466 ---------- 

467 j : float or sequence of floats 

468 Julian dates (days relative to 4713 BC Jan 1, 12:00:00 Julian 

469 calendar or 4714 BC Nov 24, 12:00:00, proleptic Gregorian calendar). 

470 

471 Returns 

472 ------- 

473 float or sequence of floats 

474 Matplotlib dates (days relative to `.get_epoch`). 

475 """ 

476 ep = np.datetime64(get_epoch(), 'h').astype(float) / 24. 

477 ep0 = np.datetime64('0000-12-31T00:00:00', 'h').astype(float) / 24. 

478 # Julian offset defined above is relative to 0000-12-31, but we need 

479 # relative to our current epoch: 

480 dt = JULIAN_OFFSET - ep0 + ep 

481 return np.subtract(j, dt) # Handles both scalar & nonscalar j. 

482 

483 

484def num2julian(n): 

485 """ 

486 Convert a Matplotlib date (or sequence) to a Julian date (or sequence). 

487 

488 Parameters 

489 ---------- 

490 n : float or sequence of floats 

491 Matplotlib dates (days relative to `.get_epoch`). 

492 

493 Returns 

494 ------- 

495 float or sequence of floats 

496 Julian dates (days relative to 4713 BC Jan 1, 12:00:00). 

497 """ 

498 ep = np.datetime64(get_epoch(), 'h').astype(float) / 24. 

499 ep0 = np.datetime64('0000-12-31T00:00:00', 'h').astype(float) / 24. 

500 # Julian offset defined above is relative to 0000-12-31, but we need 

501 # relative to our current epoch: 

502 dt = JULIAN_OFFSET - ep0 + ep 

503 return np.add(n, dt) # Handles both scalar & nonscalar j. 

504 

505 

506def num2date(x, tz=None): 

507 """ 

508 Convert Matplotlib dates to `~datetime.datetime` objects. 

509 

510 Parameters 

511 ---------- 

512 x : float or sequence of floats 

513 Number of days (fraction part represents hours, minutes, seconds) 

514 since the epoch. See `.get_epoch` for the 

515 epoch, which can be changed by :rc:`date.epoch` or `.set_epoch`. 

516 tz : str or `~datetime.tzinfo`, default: :rc:`timezone` 

517 Timezone of *x*. If a string, *tz* is passed to `dateutil.tz`. 

518 

519 Returns 

520 ------- 

521 `~datetime.datetime` or sequence of `~datetime.datetime` 

522 Dates are returned in timezone *tz*. 

523 

524 If *x* is a sequence, a sequence of `~datetime.datetime` objects will 

525 be returned. 

526 

527 Notes 

528 ----- 

529 The Gregorian calendar is assumed; this is not universal practice. 

530 For details, see the module docstring. 

531 """ 

532 tz = _get_tzinfo(tz) 

533 return _from_ordinalf_np_vectorized(x, tz).tolist() 

534 

535 

536_ordinalf_to_timedelta_np_vectorized = np.vectorize( 

537 lambda x: datetime.timedelta(days=x), otypes="O") 

538 

539 

540def num2timedelta(x): 

541 """ 

542 Convert number of days to a `~datetime.timedelta` object. 

543 

544 If *x* is a sequence, a sequence of `~datetime.timedelta` objects will 

545 be returned. 

546 

547 Parameters 

548 ---------- 

549 x : float, sequence of floats 

550 Number of days. The fraction part represents hours, minutes, seconds. 

551 

552 Returns 

553 ------- 

554 `datetime.timedelta` or list[`datetime.timedelta`] 

555 """ 

556 return _ordinalf_to_timedelta_np_vectorized(x).tolist() 

557 

558 

559def drange(dstart, dend, delta): 

560 """ 

561 Return a sequence of equally spaced Matplotlib dates. 

562 

563 The dates start at *dstart* and reach up to, but not including *dend*. 

564 They are spaced by *delta*. 

565 

566 Parameters 

567 ---------- 

568 dstart, dend : `~datetime.datetime` 

569 The date limits. 

570 delta : `datetime.timedelta` 

571 Spacing of the dates. 

572 

573 Returns 

574 ------- 

575 `numpy.array` 

576 A list floats representing Matplotlib dates. 

577 

578 """ 

579 f1 = date2num(dstart) 

580 f2 = date2num(dend) 

581 step = delta.total_seconds() / SEC_PER_DAY 

582 

583 # calculate the difference between dend and dstart in times of delta 

584 num = int(np.ceil((f2 - f1) / step)) 

585 

586 # calculate end of the interval which will be generated 

587 dinterval_end = dstart + num * delta 

588 

589 # ensure, that an half open interval will be generated [dstart, dend) 

590 if dinterval_end >= dend: 

591 # if the endpoint is greater than or equal to dend, 

592 # just subtract one delta 

593 dinterval_end -= delta 

594 num -= 1 

595 

596 f2 = date2num(dinterval_end) # new float-endpoint 

597 return np.linspace(f1, f2, num + 1) 

598 

599 

600def _wrap_in_tex(text): 

601 p = r'([a-zA-Z]+)' 

602 ret_text = re.sub(p, r'}$\1$\\mathdefault{', text) 

603 

604 # Braces ensure symbols are not spaced like binary operators. 

605 ret_text = ret_text.replace('-', '{-}').replace(':', '{:}') 

606 # To not concatenate space between numbers. 

607 ret_text = ret_text.replace(' ', r'\;') 

608 ret_text = '$\\mathdefault{' + ret_text + '}$' 

609 ret_text = ret_text.replace('$\\mathdefault{}$', '') 

610 return ret_text 

611 

612 

613## date tickers and formatters ### 

614 

615 

616class DateFormatter(ticker.Formatter): 

617 """ 

618 Format a tick (in days since the epoch) with a 

619 `~datetime.datetime.strftime` format string. 

620 """ 

621 

622 def __init__(self, fmt, tz=None, *, usetex=None): 

623 """ 

624 Parameters 

625 ---------- 

626 fmt : str 

627 `~datetime.datetime.strftime` format string 

628 tz : str or `~datetime.tzinfo`, default: :rc:`timezone` 

629 Ticks timezone. If a string, *tz* is passed to `dateutil.tz`. 

630 usetex : bool, default: :rc:`text.usetex` 

631 To enable/disable the use of TeX's math mode for rendering the 

632 results of the formatter. 

633 """ 

634 self.tz = _get_tzinfo(tz) 

635 self.fmt = fmt 

636 self._usetex = (usetex if usetex is not None else 

637 mpl.rcParams['text.usetex']) 

638 

639 def __call__(self, x, pos=0): 

640 result = num2date(x, self.tz).strftime(self.fmt) 

641 return _wrap_in_tex(result) if self._usetex else result 

642 

643 def set_tzinfo(self, tz): 

644 self.tz = _get_tzinfo(tz) 

645 

646 

647class ConciseDateFormatter(ticker.Formatter): 

648 """ 

649 A `.Formatter` which attempts to figure out the best format to use for the 

650 date, and to make it as compact as possible, but still be complete. This is 

651 most useful when used with the `AutoDateLocator`:: 

652 

653 >>> locator = AutoDateLocator() 

654 >>> formatter = ConciseDateFormatter(locator) 

655 

656 Parameters 

657 ---------- 

658 locator : `.ticker.Locator` 

659 Locator that this axis is using. 

660 

661 tz : str or `~datetime.tzinfo`, default: :rc:`timezone` 

662 Ticks timezone, passed to `.dates.num2date`. 

663 

664 formats : list of 6 strings, optional 

665 Format strings for 6 levels of tick labelling: mostly years, 

666 months, days, hours, minutes, and seconds. Strings use 

667 the same format codes as `~datetime.datetime.strftime`. Default is 

668 ``['%Y', '%b', '%d', '%H:%M', '%H:%M', '%S.%f']`` 

669 

670 zero_formats : list of 6 strings, optional 

671 Format strings for tick labels that are "zeros" for a given tick 

672 level. For instance, if most ticks are months, ticks around 1 Jan 2005 

673 will be labeled "Dec", "2005", "Feb". The default is 

674 ``['', '%Y', '%b', '%b-%d', '%H:%M', '%H:%M']`` 

675 

676 offset_formats : list of 6 strings, optional 

677 Format strings for the 6 levels that is applied to the "offset" 

678 string found on the right side of an x-axis, or top of a y-axis. 

679 Combined with the tick labels this should completely specify the 

680 date. The default is:: 

681 

682 ['', '%Y', '%Y-%b', '%Y-%b-%d', '%Y-%b-%d', '%Y-%b-%d %H:%M'] 

683 

684 show_offset : bool, default: True 

685 Whether to show the offset or not. 

686 

687 usetex : bool, default: :rc:`text.usetex` 

688 To enable/disable the use of TeX's math mode for rendering the results 

689 of the formatter. 

690 

691 Examples 

692 -------- 

693 See :doc:`/gallery/ticks/date_concise_formatter` 

694 

695 .. plot:: 

696 

697 import datetime 

698 import matplotlib.dates as mdates 

699 

700 base = datetime.datetime(2005, 2, 1) 

701 dates = np.array([base + datetime.timedelta(hours=(2 * i)) 

702 for i in range(732)]) 

703 N = len(dates) 

704 np.random.seed(19680801) 

705 y = np.cumsum(np.random.randn(N)) 

706 

707 fig, ax = plt.subplots(constrained_layout=True) 

708 locator = mdates.AutoDateLocator() 

709 formatter = mdates.ConciseDateFormatter(locator) 

710 ax.xaxis.set_major_locator(locator) 

711 ax.xaxis.set_major_formatter(formatter) 

712 

713 ax.plot(dates, y) 

714 ax.set_title('Concise Date Formatter') 

715 

716 """ 

717 

718 def __init__(self, locator, tz=None, formats=None, offset_formats=None, 

719 zero_formats=None, show_offset=True, *, usetex=None): 

720 """ 

721 Autoformat the date labels. The default format is used to form an 

722 initial string, and then redundant elements are removed. 

723 """ 

724 self._locator = locator 

725 self._tz = tz 

726 self.defaultfmt = '%Y' 

727 # there are 6 levels with each level getting a specific format 

728 # 0: mostly years, 1: months, 2: days, 

729 # 3: hours, 4: minutes, 5: seconds 

730 if formats: 

731 if len(formats) != 6: 

732 raise ValueError('formats argument must be a list of ' 

733 '6 format strings (or None)') 

734 self.formats = formats 

735 else: 

736 self.formats = ['%Y', # ticks are mostly years 

737 '%b', # ticks are mostly months 

738 '%d', # ticks are mostly days 

739 '%H:%M', # hrs 

740 '%H:%M', # min 

741 '%S.%f', # secs 

742 ] 

743 # fmt for zeros ticks at this level. These are 

744 # ticks that should be labeled w/ info the level above. 

745 # like 1 Jan can just be labelled "Jan". 02:02:00 can 

746 # just be labeled 02:02. 

747 if zero_formats: 

748 if len(zero_formats) != 6: 

749 raise ValueError('zero_formats argument must be a list of ' 

750 '6 format strings (or None)') 

751 self.zero_formats = zero_formats 

752 elif formats: 

753 # use the users formats for the zero tick formats 

754 self.zero_formats = [''] + self.formats[:-1] 

755 else: 

756 # make the defaults a bit nicer: 

757 self.zero_formats = [''] + self.formats[:-1] 

758 self.zero_formats[3] = '%b-%d' 

759 

760 if offset_formats: 

761 if len(offset_formats) != 6: 

762 raise ValueError('offset_formats argument must be a list of ' 

763 '6 format strings (or None)') 

764 self.offset_formats = offset_formats 

765 else: 

766 self.offset_formats = ['', 

767 '%Y', 

768 '%Y-%b', 

769 '%Y-%b-%d', 

770 '%Y-%b-%d', 

771 '%Y-%b-%d %H:%M'] 

772 self.offset_string = '' 

773 self.show_offset = show_offset 

774 self._usetex = (usetex if usetex is not None else 

775 mpl.rcParams['text.usetex']) 

776 

777 def __call__(self, x, pos=None): 

778 formatter = DateFormatter(self.defaultfmt, self._tz, 

779 usetex=self._usetex) 

780 return formatter(x, pos=pos) 

781 

782 def format_ticks(self, values): 

783 tickdatetime = [num2date(value, tz=self._tz) for value in values] 

784 tickdate = np.array([tdt.timetuple()[:6] for tdt in tickdatetime]) 

785 

786 # basic algorithm: 

787 # 1) only display a part of the date if it changes over the ticks. 

788 # 2) don't display the smaller part of the date if: 

789 # it is always the same or if it is the start of the 

790 # year, month, day etc. 

791 # fmt for most ticks at this level 

792 fmts = self.formats 

793 # format beginnings of days, months, years, etc. 

794 zerofmts = self.zero_formats 

795 # offset fmt are for the offset in the upper left of the 

796 # or lower right of the axis. 

797 offsetfmts = self.offset_formats 

798 show_offset = self.show_offset 

799 

800 # determine the level we will label at: 

801 # mostly 0: years, 1: months, 2: days, 

802 # 3: hours, 4: minutes, 5: seconds, 6: microseconds 

803 for level in range(5, -1, -1): 

804 unique = np.unique(tickdate[:, level]) 

805 if len(unique) > 1: 

806 # if 1 is included in unique, the year is shown in ticks 

807 if level < 2 and np.any(unique == 1): 

808 show_offset = False 

809 break 

810 elif level == 0: 

811 # all tickdate are the same, so only micros might be different 

812 # set to the most precise (6: microseconds doesn't exist...) 

813 level = 5 

814 

815 # level is the basic level we will label at. 

816 # now loop through and decide the actual ticklabels 

817 zerovals = [0, 1, 1, 0, 0, 0, 0] 

818 labels = [''] * len(tickdate) 

819 for nn in range(len(tickdate)): 

820 if level < 5: 

821 if tickdate[nn][level] == zerovals[level]: 

822 fmt = zerofmts[level] 

823 else: 

824 fmt = fmts[level] 

825 else: 

826 # special handling for seconds + microseconds 

827 if (tickdatetime[nn].second == tickdatetime[nn].microsecond 

828 == 0): 

829 fmt = zerofmts[level] 

830 else: 

831 fmt = fmts[level] 

832 labels[nn] = tickdatetime[nn].strftime(fmt) 

833 

834 # special handling of seconds and microseconds: 

835 # strip extra zeros and decimal if possible. 

836 # this is complicated by two factors. 1) we have some level-4 strings 

837 # here (i.e. 03:00, '0.50000', '1.000') 2) we would like to have the 

838 # same number of decimals for each string (i.e. 0.5 and 1.0). 

839 if level >= 5: 

840 trailing_zeros = min( 

841 (len(s) - len(s.rstrip('0')) for s in labels if '.' in s), 

842 default=None) 

843 if trailing_zeros: 

844 for nn in range(len(labels)): 

845 if '.' in labels[nn]: 

846 labels[nn] = labels[nn][:-trailing_zeros].rstrip('.') 

847 

848 if show_offset: 

849 # set the offset string: 

850 self.offset_string = tickdatetime[-1].strftime(offsetfmts[level]) 

851 if self._usetex: 

852 self.offset_string = _wrap_in_tex(self.offset_string) 

853 else: 

854 self.offset_string = '' 

855 

856 if self._usetex: 

857 return [_wrap_in_tex(l) for l in labels] 

858 else: 

859 return labels 

860 

861 def get_offset(self): 

862 return self.offset_string 

863 

864 def format_data_short(self, value): 

865 return num2date(value, tz=self._tz).strftime('%Y-%m-%d %H:%M:%S') 

866 

867 

868class AutoDateFormatter(ticker.Formatter): 

869 """ 

870 A `.Formatter` which attempts to figure out the best format to use. This 

871 is most useful when used with the `AutoDateLocator`. 

872 

873 `.AutoDateFormatter` has a ``.scale`` dictionary that maps tick scales (the 

874 interval in days between one major tick) to format strings; this dictionary 

875 defaults to :: 

876 

877 self.scaled = { 

878 DAYS_PER_YEAR: rcParams['date.autoformatter.year'], 

879 DAYS_PER_MONTH: rcParams['date.autoformatter.month'], 

880 1: rcParams['date.autoformatter.day'], 

881 1 / HOURS_PER_DAY: rcParams['date.autoformatter.hour'], 

882 1 / MINUTES_PER_DAY: rcParams['date.autoformatter.minute'], 

883 1 / SEC_PER_DAY: rcParams['date.autoformatter.second'], 

884 1 / MUSECONDS_PER_DAY: rcParams['date.autoformatter.microsecond'], 

885 } 

886 

887 The formatter uses the format string corresponding to the lowest key in 

888 the dictionary that is greater or equal to the current scale. Dictionary 

889 entries can be customized:: 

890 

891 locator = AutoDateLocator() 

892 formatter = AutoDateFormatter(locator) 

893 formatter.scaled[1/(24*60)] = '%M:%S' # only show min and sec 

894 

895 Custom callables can also be used instead of format strings. The following 

896 example shows how to use a custom format function to strip trailing zeros 

897 from decimal seconds and adds the date to the first ticklabel:: 

898 

899 def my_format_function(x, pos=None): 

900 x = matplotlib.dates.num2date(x) 

901 if pos == 0: 

902 fmt = '%D %H:%M:%S.%f' 

903 else: 

904 fmt = '%H:%M:%S.%f' 

905 label = x.strftime(fmt) 

906 label = label.rstrip("0") 

907 label = label.rstrip(".") 

908 return label 

909 

910 formatter.scaled[1/(24*60)] = my_format_function 

911 """ 

912 

913 # This can be improved by providing some user-level direction on 

914 # how to choose the best format (precedence, etc.). 

915 

916 # Perhaps a 'struct' that has a field for each time-type where a 

917 # zero would indicate "don't show" and a number would indicate 

918 # "show" with some sort of priority. Same priorities could mean 

919 # show all with the same priority. 

920 

921 # Or more simply, perhaps just a format string for each 

922 # possibility... 

923 

924 def __init__(self, locator, tz=None, defaultfmt='%Y-%m-%d', *, 

925 usetex=None): 

926 """ 

927 Autoformat the date labels. 

928 

929 Parameters 

930 ---------- 

931 locator : `.ticker.Locator` 

932 Locator that this axis is using. 

933 

934 tz : str or `~datetime.tzinfo`, default: :rc:`timezone` 

935 Ticks timezone. If a string, *tz* is passed to `dateutil.tz`. 

936 

937 defaultfmt : str 

938 The default format to use if none of the values in ``self.scaled`` 

939 are greater than the unit returned by ``locator._get_unit()``. 

940 

941 usetex : bool, default: :rc:`text.usetex` 

942 To enable/disable the use of TeX's math mode for rendering the 

943 results of the formatter. If any entries in ``self.scaled`` are set 

944 as functions, then it is up to the customized function to enable or 

945 disable TeX's math mode itself. 

946 """ 

947 self._locator = locator 

948 self._tz = tz 

949 self.defaultfmt = defaultfmt 

950 self._formatter = DateFormatter(self.defaultfmt, tz) 

951 rcParams = mpl.rcParams 

952 self._usetex = (usetex if usetex is not None else 

953 mpl.rcParams['text.usetex']) 

954 self.scaled = { 

955 DAYS_PER_YEAR: rcParams['date.autoformatter.year'], 

956 DAYS_PER_MONTH: rcParams['date.autoformatter.month'], 

957 1: rcParams['date.autoformatter.day'], 

958 1 / HOURS_PER_DAY: rcParams['date.autoformatter.hour'], 

959 1 / MINUTES_PER_DAY: rcParams['date.autoformatter.minute'], 

960 1 / SEC_PER_DAY: rcParams['date.autoformatter.second'], 

961 1 / MUSECONDS_PER_DAY: rcParams['date.autoformatter.microsecond'] 

962 } 

963 

964 def _set_locator(self, locator): 

965 self._locator = locator 

966 

967 def __call__(self, x, pos=None): 

968 try: 

969 locator_unit_scale = float(self._locator._get_unit()) 

970 except AttributeError: 

971 locator_unit_scale = 1 

972 # Pick the first scale which is greater than the locator unit. 

973 fmt = next((fmt for scale, fmt in sorted(self.scaled.items()) 

974 if scale >= locator_unit_scale), 

975 self.defaultfmt) 

976 

977 if isinstance(fmt, str): 

978 self._formatter = DateFormatter(fmt, self._tz, usetex=self._usetex) 

979 result = self._formatter(x, pos) 

980 elif callable(fmt): 

981 result = fmt(x, pos) 

982 else: 

983 raise TypeError('Unexpected type passed to {0!r}.'.format(self)) 

984 

985 return result 

986 

987 

988class rrulewrapper: 

989 """ 

990 A simple wrapper around a `dateutil.rrule` allowing flexible 

991 date tick specifications. 

992 """ 

993 def __init__(self, freq, tzinfo=None, **kwargs): 

994 """ 

995 Parameters 

996 ---------- 

997 freq : {YEARLY, MONTHLY, WEEKLY, DAILY, HOURLY, MINUTELY, SECONDLY} 

998 Tick frequency. These constants are defined in `dateutil.rrule`, 

999 but they are accessible from `matplotlib.dates` as well. 

1000 tzinfo : `datetime.tzinfo`, optional 

1001 Time zone information. The default is None. 

1002 **kwargs 

1003 Additional keyword arguments are passed to the `dateutil.rrule`. 

1004 """ 

1005 kwargs['freq'] = freq 

1006 self._base_tzinfo = tzinfo 

1007 

1008 self._update_rrule(**kwargs) 

1009 

1010 def set(self, **kwargs): 

1011 """Set parameters for an existing wrapper.""" 

1012 self._construct.update(kwargs) 

1013 

1014 self._update_rrule(**self._construct) 

1015 

1016 def _update_rrule(self, **kwargs): 

1017 tzinfo = self._base_tzinfo 

1018 

1019 # rrule does not play nicely with timezones - especially pytz time 

1020 # zones, it's best to use naive zones and attach timezones once the 

1021 # datetimes are returned 

1022 if 'dtstart' in kwargs: 

1023 dtstart = kwargs['dtstart'] 

1024 if dtstart.tzinfo is not None: 

1025 if tzinfo is None: 

1026 tzinfo = dtstart.tzinfo 

1027 else: 

1028 dtstart = dtstart.astimezone(tzinfo) 

1029 

1030 kwargs['dtstart'] = dtstart.replace(tzinfo=None) 

1031 

1032 if 'until' in kwargs: 

1033 until = kwargs['until'] 

1034 if until.tzinfo is not None: 

1035 if tzinfo is not None: 

1036 until = until.astimezone(tzinfo) 

1037 else: 

1038 raise ValueError('until cannot be aware if dtstart ' 

1039 'is naive and tzinfo is None') 

1040 

1041 kwargs['until'] = until.replace(tzinfo=None) 

1042 

1043 self._construct = kwargs.copy() 

1044 self._tzinfo = tzinfo 

1045 self._rrule = rrule(**self._construct) 

1046 

1047 def _attach_tzinfo(self, dt, tzinfo): 

1048 # pytz zones are attached by "localizing" the datetime 

1049 if hasattr(tzinfo, 'localize'): 

1050 return tzinfo.localize(dt, is_dst=True) 

1051 

1052 return dt.replace(tzinfo=tzinfo) 

1053 

1054 def _aware_return_wrapper(self, f, returns_list=False): 

1055 """Decorator function that allows rrule methods to handle tzinfo.""" 

1056 # This is only necessary if we're actually attaching a tzinfo 

1057 if self._tzinfo is None: 

1058 return f 

1059 

1060 # All datetime arguments must be naive. If they are not naive, they are 

1061 # converted to the _tzinfo zone before dropping the zone. 

1062 def normalize_arg(arg): 

1063 if isinstance(arg, datetime.datetime) and arg.tzinfo is not None: 

1064 if arg.tzinfo is not self._tzinfo: 

1065 arg = arg.astimezone(self._tzinfo) 

1066 

1067 return arg.replace(tzinfo=None) 

1068 

1069 return arg 

1070 

1071 def normalize_args(args, kwargs): 

1072 args = tuple(normalize_arg(arg) for arg in args) 

1073 kwargs = {kw: normalize_arg(arg) for kw, arg in kwargs.items()} 

1074 

1075 return args, kwargs 

1076 

1077 # There are two kinds of functions we care about - ones that return 

1078 # dates and ones that return lists of dates. 

1079 if not returns_list: 

1080 def inner_func(*args, **kwargs): 

1081 args, kwargs = normalize_args(args, kwargs) 

1082 dt = f(*args, **kwargs) 

1083 return self._attach_tzinfo(dt, self._tzinfo) 

1084 else: 

1085 def inner_func(*args, **kwargs): 

1086 args, kwargs = normalize_args(args, kwargs) 

1087 dts = f(*args, **kwargs) 

1088 return [self._attach_tzinfo(dt, self._tzinfo) for dt in dts] 

1089 

1090 return functools.wraps(f)(inner_func) 

1091 

1092 def __getattr__(self, name): 

1093 if name in self.__dict__: 

1094 return self.__dict__[name] 

1095 

1096 f = getattr(self._rrule, name) 

1097 

1098 if name in {'after', 'before'}: 

1099 return self._aware_return_wrapper(f) 

1100 elif name in {'xafter', 'xbefore', 'between'}: 

1101 return self._aware_return_wrapper(f, returns_list=True) 

1102 else: 

1103 return f 

1104 

1105 def __setstate__(self, state): 

1106 self.__dict__.update(state) 

1107 

1108 

1109class DateLocator(ticker.Locator): 

1110 """ 

1111 Determines the tick locations when plotting dates. 

1112 

1113 This class is subclassed by other Locators and 

1114 is not meant to be used on its own. 

1115 """ 

1116 hms0d = {'byhour': 0, 'byminute': 0, 'bysecond': 0} 

1117 

1118 def __init__(self, tz=None): 

1119 """ 

1120 Parameters 

1121 ---------- 

1122 tz : str or `~datetime.tzinfo`, default: :rc:`timezone` 

1123 Ticks timezone. If a string, *tz* is passed to `dateutil.tz`. 

1124 """ 

1125 self.tz = _get_tzinfo(tz) 

1126 

1127 def set_tzinfo(self, tz): 

1128 """ 

1129 Set timezone info. 

1130 

1131 Parameters 

1132 ---------- 

1133 tz : str or `~datetime.tzinfo`, default: :rc:`timezone` 

1134 Ticks timezone. If a string, *tz* is passed to `dateutil.tz`. 

1135 """ 

1136 self.tz = _get_tzinfo(tz) 

1137 

1138 def datalim_to_dt(self): 

1139 """Convert axis data interval to datetime objects.""" 

1140 dmin, dmax = self.axis.get_data_interval() 

1141 if dmin > dmax: 

1142 dmin, dmax = dmax, dmin 

1143 

1144 return num2date(dmin, self.tz), num2date(dmax, self.tz) 

1145 

1146 def viewlim_to_dt(self): 

1147 """Convert the view interval to datetime objects.""" 

1148 vmin, vmax = self.axis.get_view_interval() 

1149 if vmin > vmax: 

1150 vmin, vmax = vmax, vmin 

1151 return num2date(vmin, self.tz), num2date(vmax, self.tz) 

1152 

1153 def _get_unit(self): 

1154 """ 

1155 Return how many days a unit of the locator is; used for 

1156 intelligent autoscaling. 

1157 """ 

1158 return 1 

1159 

1160 def _get_interval(self): 

1161 """ 

1162 Return the number of units for each tick. 

1163 """ 

1164 return 1 

1165 

1166 def nonsingular(self, vmin, vmax): 

1167 """ 

1168 Given the proposed upper and lower extent, adjust the range 

1169 if it is too close to being singular (i.e. a range of ~0). 

1170 """ 

1171 if not np.isfinite(vmin) or not np.isfinite(vmax): 

1172 # Except if there is no data, then use 1970 as default. 

1173 return (date2num(datetime.date(1970, 1, 1)), 

1174 date2num(datetime.date(1970, 1, 2))) 

1175 if vmax < vmin: 

1176 vmin, vmax = vmax, vmin 

1177 unit = self._get_unit() 

1178 interval = self._get_interval() 

1179 if abs(vmax - vmin) < 1e-6: 

1180 vmin -= 2 * unit * interval 

1181 vmax += 2 * unit * interval 

1182 return vmin, vmax 

1183 

1184 

1185class RRuleLocator(DateLocator): 

1186 # use the dateutil rrule instance 

1187 

1188 def __init__(self, o, tz=None): 

1189 super().__init__(tz) 

1190 self.rule = o 

1191 

1192 def __call__(self): 

1193 # if no data have been set, this will tank with a ValueError 

1194 try: 

1195 dmin, dmax = self.viewlim_to_dt() 

1196 except ValueError: 

1197 return [] 

1198 

1199 return self.tick_values(dmin, dmax) 

1200 

1201 def tick_values(self, vmin, vmax): 

1202 start, stop = self._create_rrule(vmin, vmax) 

1203 dates = self.rule.between(start, stop, True) 

1204 if len(dates) == 0: 

1205 return date2num([vmin, vmax]) 

1206 return self.raise_if_exceeds(date2num(dates)) 

1207 

1208 def _create_rrule(self, vmin, vmax): 

1209 # set appropriate rrule dtstart and until and return 

1210 # start and end 

1211 delta = relativedelta(vmax, vmin) 

1212 

1213 # We need to cap at the endpoints of valid datetime 

1214 try: 

1215 start = vmin - delta 

1216 except (ValueError, OverflowError): 

1217 # cap 

1218 start = datetime.datetime(1, 1, 1, 0, 0, 0, 

1219 tzinfo=datetime.timezone.utc) 

1220 

1221 try: 

1222 stop = vmax + delta 

1223 except (ValueError, OverflowError): 

1224 # cap 

1225 stop = datetime.datetime(9999, 12, 31, 23, 59, 59, 

1226 tzinfo=datetime.timezone.utc) 

1227 

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

1229 

1230 return vmin, vmax 

1231 

1232 def _get_unit(self): 

1233 # docstring inherited 

1234 freq = self.rule._rrule._freq 

1235 return self.get_unit_generic(freq) 

1236 

1237 @staticmethod 

1238 def get_unit_generic(freq): 

1239 if freq == YEARLY: 

1240 return DAYS_PER_YEAR 

1241 elif freq == MONTHLY: 

1242 return DAYS_PER_MONTH 

1243 elif freq == WEEKLY: 

1244 return DAYS_PER_WEEK 

1245 elif freq == DAILY: 

1246 return 1.0 

1247 elif freq == HOURLY: 

1248 return 1.0 / HOURS_PER_DAY 

1249 elif freq == MINUTELY: 

1250 return 1.0 / MINUTES_PER_DAY 

1251 elif freq == SECONDLY: 

1252 return 1.0 / SEC_PER_DAY 

1253 else: 

1254 # error 

1255 return -1 # or should this just return '1'? 

1256 

1257 def _get_interval(self): 

1258 return self.rule._rrule._interval 

1259 

1260 

1261class AutoDateLocator(DateLocator): 

1262 """ 

1263 On autoscale, this class picks the best `DateLocator` to set the view 

1264 limits and the tick locations. 

1265 

1266 Attributes 

1267 ---------- 

1268 intervald : dict 

1269 

1270 Mapping of tick frequencies to multiples allowed for that ticking. 

1271 The default is :: 

1272 

1273 self.intervald = { 

1274 YEARLY : [1, 2, 4, 5, 10, 20, 40, 50, 100, 200, 400, 500, 

1275 1000, 2000, 4000, 5000, 10000], 

1276 MONTHLY : [1, 2, 3, 4, 6], 

1277 DAILY : [1, 2, 3, 7, 14, 21], 

1278 HOURLY : [1, 2, 3, 4, 6, 12], 

1279 MINUTELY: [1, 5, 10, 15, 30], 

1280 SECONDLY: [1, 5, 10, 15, 30], 

1281 MICROSECONDLY: [1, 2, 5, 10, 20, 50, 100, 200, 500, 

1282 1000, 2000, 5000, 10000, 20000, 50000, 

1283 100000, 200000, 500000, 1000000], 

1284 } 

1285 

1286 where the keys are defined in `dateutil.rrule`. 

1287 

1288 The interval is used to specify multiples that are appropriate for 

1289 the frequency of ticking. For instance, every 7 days is sensible 

1290 for daily ticks, but for minutes/seconds, 15 or 30 make sense. 

1291 

1292 When customizing, you should only modify the values for the existing 

1293 keys. You should not add or delete entries. 

1294 

1295 Example for forcing ticks every 3 hours:: 

1296 

1297 locator = AutoDateLocator() 

1298 locator.intervald[HOURLY] = [3] # only show every 3 hours 

1299 """ 

1300 

1301 def __init__(self, tz=None, minticks=5, maxticks=None, 

1302 interval_multiples=True): 

1303 """ 

1304 Parameters 

1305 ---------- 

1306 tz : str or `~datetime.tzinfo`, default: :rc:`timezone` 

1307 Ticks timezone. If a string, *tz* is passed to `dateutil.tz`. 

1308 minticks : int 

1309 The minimum number of ticks desired; controls whether ticks occur 

1310 yearly, monthly, etc. 

1311 maxticks : int 

1312 The maximum number of ticks desired; controls the interval between 

1313 ticks (ticking every other, every 3, etc.). For fine-grained 

1314 control, this can be a dictionary mapping individual rrule 

1315 frequency constants (YEARLY, MONTHLY, etc.) to their own maximum 

1316 number of ticks. This can be used to keep the number of ticks 

1317 appropriate to the format chosen in `AutoDateFormatter`. Any 

1318 frequency not specified in this dictionary is given a default 

1319 value. 

1320 interval_multiples : bool, default: True 

1321 Whether ticks should be chosen to be multiple of the interval, 

1322 locking them to 'nicer' locations. For example, this will force 

1323 the ticks to be at hours 0, 6, 12, 18 when hourly ticking is done 

1324 at 6 hour intervals. 

1325 """ 

1326 super().__init__(tz=tz) 

1327 self._freq = YEARLY 

1328 self._freqs = [YEARLY, MONTHLY, DAILY, HOURLY, MINUTELY, 

1329 SECONDLY, MICROSECONDLY] 

1330 self.minticks = minticks 

1331 

1332 self.maxticks = {YEARLY: 11, MONTHLY: 12, DAILY: 11, HOURLY: 12, 

1333 MINUTELY: 11, SECONDLY: 11, MICROSECONDLY: 8} 

1334 if maxticks is not None: 

1335 try: 

1336 self.maxticks.update(maxticks) 

1337 except TypeError: 

1338 # Assume we were given an integer. Use this as the maximum 

1339 # number of ticks for every frequency and create a 

1340 # dictionary for this 

1341 self.maxticks = dict.fromkeys(self._freqs, maxticks) 

1342 self.interval_multiples = interval_multiples 

1343 self.intervald = { 

1344 YEARLY: [1, 2, 4, 5, 10, 20, 40, 50, 100, 200, 400, 500, 

1345 1000, 2000, 4000, 5000, 10000], 

1346 MONTHLY: [1, 2, 3, 4, 6], 

1347 DAILY: [1, 2, 3, 7, 14, 21], 

1348 HOURLY: [1, 2, 3, 4, 6, 12], 

1349 MINUTELY: [1, 5, 10, 15, 30], 

1350 SECONDLY: [1, 5, 10, 15, 30], 

1351 MICROSECONDLY: [1, 2, 5, 10, 20, 50, 100, 200, 500, 1000, 2000, 

1352 5000, 10000, 20000, 50000, 100000, 200000, 500000, 

1353 1000000], 

1354 } 

1355 if interval_multiples: 

1356 # Swap "3" for "4" in the DAILY list; If we use 3 we get bad 

1357 # tick loc for months w/ 31 days: 1, 4, ..., 28, 31, 1 

1358 # If we use 4 then we get: 1, 5, ... 25, 29, 1 

1359 self.intervald[DAILY] = [1, 2, 4, 7, 14] 

1360 

1361 self._byranges = [None, range(1, 13), range(1, 32), 

1362 range(0, 24), range(0, 60), range(0, 60), None] 

1363 

1364 def __call__(self): 

1365 # docstring inherited 

1366 dmin, dmax = self.viewlim_to_dt() 

1367 locator = self.get_locator(dmin, dmax) 

1368 return locator() 

1369 

1370 def tick_values(self, vmin, vmax): 

1371 return self.get_locator(vmin, vmax).tick_values(vmin, vmax) 

1372 

1373 def nonsingular(self, vmin, vmax): 

1374 # whatever is thrown at us, we can scale the unit. 

1375 # But default nonsingular date plots at an ~4 year period. 

1376 if not np.isfinite(vmin) or not np.isfinite(vmax): 

1377 # Except if there is no data, then use 1970 as default. 

1378 return (date2num(datetime.date(1970, 1, 1)), 

1379 date2num(datetime.date(1970, 1, 2))) 

1380 if vmax < vmin: 

1381 vmin, vmax = vmax, vmin 

1382 if vmin == vmax: 

1383 vmin = vmin - DAYS_PER_YEAR * 2 

1384 vmax = vmax + DAYS_PER_YEAR * 2 

1385 return vmin, vmax 

1386 

1387 def _get_unit(self): 

1388 if self._freq in [MICROSECONDLY]: 

1389 return 1. / MUSECONDS_PER_DAY 

1390 else: 

1391 return RRuleLocator.get_unit_generic(self._freq) 

1392 

1393 def get_locator(self, dmin, dmax): 

1394 """Pick the best locator based on a distance.""" 

1395 delta = relativedelta(dmax, dmin) 

1396 tdelta = dmax - dmin 

1397 

1398 # take absolute difference 

1399 if dmin > dmax: 

1400 delta = -delta 

1401 tdelta = -tdelta 

1402 # The following uses a mix of calls to relativedelta and timedelta 

1403 # methods because there is incomplete overlap in the functionality of 

1404 # these similar functions, and it's best to avoid doing our own math 

1405 # whenever possible. 

1406 numYears = float(delta.years) 

1407 numMonths = numYears * MONTHS_PER_YEAR + delta.months 

1408 numDays = tdelta.days # Avoids estimates of days/month, days/year. 

1409 numHours = numDays * HOURS_PER_DAY + delta.hours 

1410 numMinutes = numHours * MIN_PER_HOUR + delta.minutes 

1411 numSeconds = np.floor(tdelta.total_seconds()) 

1412 numMicroseconds = np.floor(tdelta.total_seconds() * 1e6) 

1413 

1414 nums = [numYears, numMonths, numDays, numHours, numMinutes, 

1415 numSeconds, numMicroseconds] 

1416 

1417 use_rrule_locator = [True] * 6 + [False] 

1418 

1419 # Default setting of bymonth, etc. to pass to rrule 

1420 # [unused (for year), bymonth, bymonthday, byhour, byminute, 

1421 # bysecond, unused (for microseconds)] 

1422 byranges = [None, 1, 1, 0, 0, 0, None] 

1423 

1424 # Loop over all the frequencies and try to find one that gives at 

1425 # least a minticks tick positions. Once this is found, look for 

1426 # an interval from a list specific to that frequency that gives no 

1427 # more than maxticks tick positions. Also, set up some ranges 

1428 # (bymonth, etc.) as appropriate to be passed to rrulewrapper. 

1429 for i, (freq, num) in enumerate(zip(self._freqs, nums)): 

1430 # If this particular frequency doesn't give enough ticks, continue 

1431 if num < self.minticks: 

1432 # Since we're not using this particular frequency, set 

1433 # the corresponding by_ to None so the rrule can act as 

1434 # appropriate 

1435 byranges[i] = None 

1436 continue 

1437 

1438 # Find the first available interval that doesn't give too many 

1439 # ticks 

1440 for interval in self.intervald[freq]: 

1441 if num <= interval * (self.maxticks[freq] - 1): 

1442 break 

1443 else: 

1444 if not (self.interval_multiples and freq == DAILY): 

1445 _api.warn_external( 

1446 f"AutoDateLocator was unable to pick an appropriate " 

1447 f"interval for this date range. It may be necessary " 

1448 f"to add an interval value to the AutoDateLocator's " 

1449 f"intervald dictionary. Defaulting to {interval}.") 

1450 

1451 # Set some parameters as appropriate 

1452 self._freq = freq 

1453 

1454 if self._byranges[i] and self.interval_multiples: 

1455 byranges[i] = self._byranges[i][::interval] 

1456 if i in (DAILY, WEEKLY): 

1457 if interval == 14: 

1458 # just make first and 15th. Avoids 30th. 

1459 byranges[i] = [1, 15] 

1460 elif interval == 7: 

1461 byranges[i] = [1, 8, 15, 22] 

1462 

1463 interval = 1 

1464 else: 

1465 byranges[i] = self._byranges[i] 

1466 break 

1467 else: 

1468 interval = 1 

1469 

1470 if (freq == YEARLY) and self.interval_multiples: 

1471 locator = YearLocator(interval, tz=self.tz) 

1472 elif use_rrule_locator[i]: 

1473 _, bymonth, bymonthday, byhour, byminute, bysecond, _ = byranges 

1474 rrule = rrulewrapper(self._freq, interval=interval, 

1475 dtstart=dmin, until=dmax, 

1476 bymonth=bymonth, bymonthday=bymonthday, 

1477 byhour=byhour, byminute=byminute, 

1478 bysecond=bysecond) 

1479 

1480 locator = RRuleLocator(rrule, tz=self.tz) 

1481 else: 

1482 locator = MicrosecondLocator(interval, tz=self.tz) 

1483 if date2num(dmin) > 70 * 365 and interval < 1000: 

1484 _api.warn_external( 

1485 'Plotting microsecond time intervals for dates far from ' 

1486 f'the epoch (time origin: {get_epoch()}) is not well-' 

1487 'supported. See matplotlib.dates.set_epoch to change the ' 

1488 'epoch.') 

1489 

1490 locator.set_axis(self.axis) 

1491 return locator 

1492 

1493 

1494class YearLocator(RRuleLocator): 

1495 """ 

1496 Make ticks on a given day of each year that is a multiple of base. 

1497 

1498 Examples:: 

1499 

1500 # Tick every year on Jan 1st 

1501 locator = YearLocator() 

1502 

1503 # Tick every 5 years on July 4th 

1504 locator = YearLocator(5, month=7, day=4) 

1505 """ 

1506 def __init__(self, base=1, month=1, day=1, tz=None): 

1507 """ 

1508 Parameters 

1509 ---------- 

1510 base : int, default: 1 

1511 Mark ticks every *base* years. 

1512 month : int, default: 1 

1513 The month on which to place the ticks, starting from 1. Default is 

1514 January. 

1515 day : int, default: 1 

1516 The day on which to place the ticks. 

1517 tz : str or `~datetime.tzinfo`, default: :rc:`timezone` 

1518 Ticks timezone. If a string, *tz* is passed to `dateutil.tz`. 

1519 """ 

1520 rule = rrulewrapper(YEARLY, interval=base, bymonth=month, 

1521 bymonthday=day, **self.hms0d) 

1522 super().__init__(rule, tz=tz) 

1523 self.base = ticker._Edge_integer(base, 0) 

1524 

1525 def _create_rrule(self, vmin, vmax): 

1526 # 'start' needs to be a multiple of the interval to create ticks on 

1527 # interval multiples when the tick frequency is YEARLY 

1528 ymin = max(self.base.le(vmin.year) * self.base.step, 1) 

1529 ymax = min(self.base.ge(vmax.year) * self.base.step, 9999) 

1530 

1531 c = self.rule._construct 

1532 replace = {'year': ymin, 

1533 'month': c.get('bymonth', 1), 

1534 'day': c.get('bymonthday', 1), 

1535 'hour': 0, 'minute': 0, 'second': 0} 

1536 

1537 start = vmin.replace(**replace) 

1538 stop = start.replace(year=ymax) 

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

1540 

1541 return start, stop 

1542 

1543 

1544class MonthLocator(RRuleLocator): 

1545 """ 

1546 Make ticks on occurrences of each month, e.g., 1, 3, 12. 

1547 """ 

1548 def __init__(self, bymonth=None, bymonthday=1, interval=1, tz=None): 

1549 """ 

1550 Parameters 

1551 ---------- 

1552 bymonth : int or list of int, default: all months 

1553 Ticks will be placed on every month in *bymonth*. Default is 

1554 ``range(1, 13)``, i.e. every month. 

1555 bymonthday : int, default: 1 

1556 The day on which to place the ticks. 

1557 interval : int, default: 1 

1558 The interval between each iteration. For example, if 

1559 ``interval=2``, mark every second occurrence. 

1560 tz : str or `~datetime.tzinfo`, default: :rc:`timezone` 

1561 Ticks timezone. If a string, *tz* is passed to `dateutil.tz`. 

1562 """ 

1563 if bymonth is None: 

1564 bymonth = range(1, 13) 

1565 

1566 rule = rrulewrapper(MONTHLY, bymonth=bymonth, bymonthday=bymonthday, 

1567 interval=interval, **self.hms0d) 

1568 super().__init__(rule, tz=tz) 

1569 

1570 

1571class WeekdayLocator(RRuleLocator): 

1572 """ 

1573 Make ticks on occurrences of each weekday. 

1574 """ 

1575 

1576 def __init__(self, byweekday=1, interval=1, tz=None): 

1577 """ 

1578 Parameters 

1579 ---------- 

1580 byweekday : int or list of int, default: all days 

1581 Ticks will be placed on every weekday in *byweekday*. Default is 

1582 every day. 

1583 

1584 Elements of *byweekday* must be one of MO, TU, WE, TH, FR, SA, 

1585 SU, the constants from :mod:`dateutil.rrule`, which have been 

1586 imported into the :mod:`matplotlib.dates` namespace. 

1587 interval : int, default: 1 

1588 The interval between each iteration. For example, if 

1589 ``interval=2``, mark every second occurrence. 

1590 tz : str or `~datetime.tzinfo`, default: :rc:`timezone` 

1591 Ticks timezone. If a string, *tz* is passed to `dateutil.tz`. 

1592 """ 

1593 rule = rrulewrapper(DAILY, byweekday=byweekday, 

1594 interval=interval, **self.hms0d) 

1595 super().__init__(rule, tz=tz) 

1596 

1597 

1598class DayLocator(RRuleLocator): 

1599 """ 

1600 Make ticks on occurrences of each day of the month. For example, 

1601 1, 15, 30. 

1602 """ 

1603 def __init__(self, bymonthday=None, interval=1, tz=None): 

1604 """ 

1605 Parameters 

1606 ---------- 

1607 bymonthday : int or list of int, default: all days 

1608 Ticks will be placed on every day in *bymonthday*. Default is 

1609 ``bymonthday=range(1, 32)``, i.e., every day of the month. 

1610 interval : int, default: 1 

1611 The interval between each iteration. For example, if 

1612 ``interval=2``, mark every second occurrence. 

1613 tz : str or `~datetime.tzinfo`, default: :rc:`timezone` 

1614 Ticks timezone. If a string, *tz* is passed to `dateutil.tz`. 

1615 """ 

1616 if interval != int(interval) or interval < 1: 

1617 raise ValueError("interval must be an integer greater than 0") 

1618 if bymonthday is None: 

1619 bymonthday = range(1, 32) 

1620 

1621 rule = rrulewrapper(DAILY, bymonthday=bymonthday, 

1622 interval=interval, **self.hms0d) 

1623 super().__init__(rule, tz=tz) 

1624 

1625 

1626class HourLocator(RRuleLocator): 

1627 """ 

1628 Make ticks on occurrences of each hour. 

1629 """ 

1630 def __init__(self, byhour=None, interval=1, tz=None): 

1631 """ 

1632 Parameters 

1633 ---------- 

1634 byhour : int or list of int, default: all hours 

1635 Ticks will be placed on every hour in *byhour*. Default is 

1636 ``byhour=range(24)``, i.e., every hour. 

1637 interval : int, default: 1 

1638 The interval between each iteration. For example, if 

1639 ``interval=2``, mark every second occurrence. 

1640 tz : str or `~datetime.tzinfo`, default: :rc:`timezone` 

1641 Ticks timezone. If a string, *tz* is passed to `dateutil.tz`. 

1642 """ 

1643 if byhour is None: 

1644 byhour = range(24) 

1645 

1646 rule = rrulewrapper(HOURLY, byhour=byhour, interval=interval, 

1647 byminute=0, bysecond=0) 

1648 super().__init__(rule, tz=tz) 

1649 

1650 

1651class MinuteLocator(RRuleLocator): 

1652 """ 

1653 Make ticks on occurrences of each minute. 

1654 """ 

1655 def __init__(self, byminute=None, interval=1, tz=None): 

1656 """ 

1657 Parameters 

1658 ---------- 

1659 byminute : int or list of int, default: all minutes 

1660 Ticks will be placed on every minute in *byminute*. Default is 

1661 ``byminute=range(60)``, i.e., every minute. 

1662 interval : int, default: 1 

1663 The interval between each iteration. For example, if 

1664 ``interval=2``, mark every second occurrence. 

1665 tz : str or `~datetime.tzinfo`, default: :rc:`timezone` 

1666 Ticks timezone. If a string, *tz* is passed to `dateutil.tz`. 

1667 """ 

1668 if byminute is None: 

1669 byminute = range(60) 

1670 

1671 rule = rrulewrapper(MINUTELY, byminute=byminute, interval=interval, 

1672 bysecond=0) 

1673 super().__init__(rule, tz=tz) 

1674 

1675 

1676class SecondLocator(RRuleLocator): 

1677 """ 

1678 Make ticks on occurrences of each second. 

1679 """ 

1680 def __init__(self, bysecond=None, interval=1, tz=None): 

1681 """ 

1682 Parameters 

1683 ---------- 

1684 bysecond : int or list of int, default: all seconds 

1685 Ticks will be placed on every second in *bysecond*. Default is 

1686 ``bysecond = range(60)``, i.e., every second. 

1687 interval : int, default: 1 

1688 The interval between each iteration. For example, if 

1689 ``interval=2``, mark every second occurrence. 

1690 tz : str or `~datetime.tzinfo`, default: :rc:`timezone` 

1691 Ticks timezone. If a string, *tz* is passed to `dateutil.tz`. 

1692 """ 

1693 if bysecond is None: 

1694 bysecond = range(60) 

1695 

1696 rule = rrulewrapper(SECONDLY, bysecond=bysecond, interval=interval) 

1697 super().__init__(rule, tz=tz) 

1698 

1699 

1700class MicrosecondLocator(DateLocator): 

1701 """ 

1702 Make ticks on regular intervals of one or more microsecond(s). 

1703 

1704 .. note:: 

1705 

1706 By default, Matplotlib uses a floating point representation of time in 

1707 days since the epoch, so plotting data with 

1708 microsecond time resolution does not work well for 

1709 dates that are far (about 70 years) from the epoch (check with 

1710 `~.dates.get_epoch`). 

1711 

1712 If you want sub-microsecond resolution time plots, it is strongly 

1713 recommended to use floating point seconds, not datetime-like 

1714 time representation. 

1715 

1716 If you really must use datetime.datetime() or similar and still 

1717 need microsecond precision, change the time origin via 

1718 `.dates.set_epoch` to something closer to the dates being plotted. 

1719 See :doc:`/gallery/ticks/date_precision_and_epochs`. 

1720 

1721 """ 

1722 def __init__(self, interval=1, tz=None): 

1723 """ 

1724 Parameters 

1725 ---------- 

1726 interval : int, default: 1 

1727 The interval between each iteration. For example, if 

1728 ``interval=2``, mark every second occurrence. 

1729 tz : str or `~datetime.tzinfo`, default: :rc:`timezone` 

1730 Ticks timezone. If a string, *tz* is passed to `dateutil.tz`. 

1731 """ 

1732 super().__init__(tz=tz) 

1733 self._interval = interval 

1734 self._wrapped_locator = ticker.MultipleLocator(interval) 

1735 

1736 def set_axis(self, axis): 

1737 self._wrapped_locator.set_axis(axis) 

1738 return super().set_axis(axis) 

1739 

1740 @_api.deprecated("3.5", alternative="`.Axis.set_view_interval`") 

1741 def set_view_interval(self, vmin, vmax): 

1742 self._wrapped_locator.set_view_interval(vmin, vmax) 

1743 return super().set_view_interval(vmin, vmax) 

1744 

1745 @_api.deprecated("3.5", alternative="`.Axis.set_data_interval`") 

1746 def set_data_interval(self, vmin, vmax): 

1747 self._wrapped_locator.set_data_interval(vmin, vmax) 

1748 return super().set_data_interval(vmin, vmax) 

1749 

1750 def __call__(self): 

1751 # if no data have been set, this will tank with a ValueError 

1752 try: 

1753 dmin, dmax = self.viewlim_to_dt() 

1754 except ValueError: 

1755 return [] 

1756 

1757 return self.tick_values(dmin, dmax) 

1758 

1759 def tick_values(self, vmin, vmax): 

1760 nmin, nmax = date2num((vmin, vmax)) 

1761 t0 = np.floor(nmin) 

1762 nmax = nmax - t0 

1763 nmin = nmin - t0 

1764 nmin *= MUSECONDS_PER_DAY 

1765 nmax *= MUSECONDS_PER_DAY 

1766 

1767 ticks = self._wrapped_locator.tick_values(nmin, nmax) 

1768 

1769 ticks = ticks / MUSECONDS_PER_DAY + t0 

1770 return ticks 

1771 

1772 def _get_unit(self): 

1773 # docstring inherited 

1774 return 1. / MUSECONDS_PER_DAY 

1775 

1776 def _get_interval(self): 

1777 # docstring inherited 

1778 return self._interval 

1779 

1780 

1781@_api.deprecated( 

1782 "3.5", 

1783 alternative="``[date2num(datetime.utcfromtimestamp(t)) for t in e]`` or " 

1784 "numpy.datetime64 types") 

1785def epoch2num(e): 

1786 """ 

1787 Convert UNIX time to days since Matplotlib epoch. 

1788 

1789 Parameters 

1790 ---------- 

1791 e : list of floats 

1792 Time in seconds since 1970-01-01. 

1793 

1794 Returns 

1795 ------- 

1796 `numpy.array` 

1797 Time in days since Matplotlib epoch (see `~.dates.get_epoch()`). 

1798 """ 

1799 

1800 dt = (np.datetime64('1970-01-01T00:00:00', 's') - 

1801 np.datetime64(get_epoch(), 's')).astype(float) 

1802 

1803 return (dt + np.asarray(e)) / SEC_PER_DAY 

1804 

1805 

1806@_api.deprecated("3.5", alternative="`num2date(e).timestamp()<.num2date>`") 

1807def num2epoch(d): 

1808 """ 

1809 Convert days since Matplotlib epoch to UNIX time. 

1810 

1811 Parameters 

1812 ---------- 

1813 d : list of floats 

1814 Time in days since Matplotlib epoch (see `~.dates.get_epoch()`). 

1815 

1816 Returns 

1817 ------- 

1818 `numpy.array` 

1819 Time in seconds since 1970-01-01. 

1820 """ 

1821 dt = (np.datetime64('1970-01-01T00:00:00', 's') - 

1822 np.datetime64(get_epoch(), 's')).astype(float) 

1823 

1824 return np.asarray(d) * SEC_PER_DAY - dt 

1825 

1826 

1827@_api.deprecated("3.6", alternative="`AutoDateLocator` and `AutoDateFormatter`" 

1828 " or vendor the code") 

1829def date_ticker_factory(span, tz=None, numticks=5): 

1830 """ 

1831 Create a date locator with *numticks* (approx) and a date formatter 

1832 for *span* in days. Return value is (locator, formatter). 

1833 """ 

1834 

1835 if span == 0: 

1836 span = 1 / HOURS_PER_DAY 

1837 

1838 mins = span * MINUTES_PER_DAY 

1839 hrs = span * HOURS_PER_DAY 

1840 days = span 

1841 wks = span / DAYS_PER_WEEK 

1842 months = span / DAYS_PER_MONTH # Approx 

1843 years = span / DAYS_PER_YEAR # Approx 

1844 

1845 if years > numticks: 

1846 locator = YearLocator(int(years / numticks), tz=tz) # define 

1847 fmt = '%Y' 

1848 elif months > numticks: 

1849 locator = MonthLocator(tz=tz) 

1850 fmt = '%b %Y' 

1851 elif wks > numticks: 

1852 locator = WeekdayLocator(tz=tz) 

1853 fmt = '%a, %b %d' 

1854 elif days > numticks: 

1855 locator = DayLocator(interval=math.ceil(days / numticks), tz=tz) 

1856 fmt = '%b %d' 

1857 elif hrs > numticks: 

1858 locator = HourLocator(interval=math.ceil(hrs / numticks), tz=tz) 

1859 fmt = '%H:%M\n%b %d' 

1860 elif mins > numticks: 

1861 locator = MinuteLocator(interval=math.ceil(mins / numticks), tz=tz) 

1862 fmt = '%H:%M:%S' 

1863 else: 

1864 locator = MinuteLocator(tz=tz) 

1865 fmt = '%H:%M:%S' 

1866 

1867 formatter = DateFormatter(fmt, tz=tz) 

1868 return locator, formatter 

1869 

1870 

1871class DateConverter(units.ConversionInterface): 

1872 """ 

1873 Converter for `datetime.date` and `datetime.datetime` data, or for 

1874 date/time data represented as it would be converted by `date2num`. 

1875 

1876 The 'unit' tag for such data is None or a `~datetime.tzinfo` instance. 

1877 """ 

1878 

1879 def __init__(self, *, interval_multiples=True): 

1880 self._interval_multiples = interval_multiples 

1881 super().__init__() 

1882 

1883 def axisinfo(self, unit, axis): 

1884 """ 

1885 Return the `~matplotlib.units.AxisInfo` for *unit*. 

1886 

1887 *unit* is a `~datetime.tzinfo` instance or None. 

1888 The *axis* argument is required but not used. 

1889 """ 

1890 tz = unit 

1891 

1892 majloc = AutoDateLocator(tz=tz, 

1893 interval_multiples=self._interval_multiples) 

1894 majfmt = AutoDateFormatter(majloc, tz=tz) 

1895 datemin = datetime.date(1970, 1, 1) 

1896 datemax = datetime.date(1970, 1, 2) 

1897 

1898 return units.AxisInfo(majloc=majloc, majfmt=majfmt, label='', 

1899 default_limits=(datemin, datemax)) 

1900 

1901 @staticmethod 

1902 def convert(value, unit, axis): 

1903 """ 

1904 If *value* is not already a number or sequence of numbers, convert it 

1905 with `date2num`. 

1906 

1907 The *unit* and *axis* arguments are not used. 

1908 """ 

1909 return date2num(value) 

1910 

1911 @staticmethod 

1912 def default_units(x, axis): 

1913 """ 

1914 Return the `~datetime.tzinfo` instance of *x* or of its first element, 

1915 or None 

1916 """ 

1917 if isinstance(x, np.ndarray): 

1918 x = x.ravel() 

1919 

1920 try: 

1921 x = cbook._safe_first_finite(x) 

1922 except (TypeError, StopIteration): 

1923 pass 

1924 

1925 try: 

1926 return x.tzinfo 

1927 except AttributeError: 

1928 pass 

1929 return None 

1930 

1931 

1932class ConciseDateConverter(DateConverter): 

1933 # docstring inherited 

1934 

1935 def __init__(self, formats=None, zero_formats=None, offset_formats=None, 

1936 show_offset=True, *, interval_multiples=True): 

1937 self._formats = formats 

1938 self._zero_formats = zero_formats 

1939 self._offset_formats = offset_formats 

1940 self._show_offset = show_offset 

1941 self._interval_multiples = interval_multiples 

1942 super().__init__() 

1943 

1944 def axisinfo(self, unit, axis): 

1945 # docstring inherited 

1946 tz = unit 

1947 majloc = AutoDateLocator(tz=tz, 

1948 interval_multiples=self._interval_multiples) 

1949 majfmt = ConciseDateFormatter(majloc, tz=tz, formats=self._formats, 

1950 zero_formats=self._zero_formats, 

1951 offset_formats=self._offset_formats, 

1952 show_offset=self._show_offset) 

1953 datemin = datetime.date(1970, 1, 1) 

1954 datemax = datetime.date(1970, 1, 2) 

1955 return units.AxisInfo(majloc=majloc, majfmt=majfmt, label='', 

1956 default_limits=(datemin, datemax)) 

1957 

1958 

1959class _SwitchableDateConverter: 

1960 """ 

1961 Helper converter-like object that generates and dispatches to 

1962 temporary ConciseDateConverter or DateConverter instances based on 

1963 :rc:`date.converter` and :rc:`date.interval_multiples`. 

1964 """ 

1965 

1966 @staticmethod 

1967 def _get_converter(): 

1968 converter_cls = { 

1969 "concise": ConciseDateConverter, "auto": DateConverter}[ 

1970 mpl.rcParams["date.converter"]] 

1971 interval_multiples = mpl.rcParams["date.interval_multiples"] 

1972 return converter_cls(interval_multiples=interval_multiples) 

1973 

1974 def axisinfo(self, *args, **kwargs): 

1975 return self._get_converter().axisinfo(*args, **kwargs) 

1976 

1977 def default_units(self, *args, **kwargs): 

1978 return self._get_converter().default_units(*args, **kwargs) 

1979 

1980 def convert(self, *args, **kwargs): 

1981 return self._get_converter().convert(*args, **kwargs) 

1982 

1983 

1984units.registry[np.datetime64] = \ 

1985 units.registry[datetime.date] = \ 

1986 units.registry[datetime.datetime] = \ 

1987 _SwitchableDateConverter()