From 2971dec3023165be7bf8ebdf80c9953389237013 Mon Sep 17 00:00:00 2001 From: Hassan Kibirige Date: Sun, 12 Apr 2015 14:36:26 -0500 Subject: [PATCH] Add tick_values method to the date Locators *Issue* Date Locators are a subclass of `matplotlib.ticker.Locator` and they should override the `tick_values` method. This method allows Locators to be queried for tick locations using two limits. Without `tick_values` this only axes with data on them can get tick locations by calling on the `Locator` object. Or the user has to go through the hussle of creating dummy axes, add data to them, then use `Locator`. *Problem* The date Locators have implemented the tick generation logic in the `__call__` method. This is in contrast with the `Locators` implemented in `matplotlib.ticker` for which the `__call__` method gets the limits from the axes and then lets `tick_values` to do the the rest. *Solution* - Have the `__call__` method of the date Locators get the limits and let `tick_values` do the calculations. - Make `MicrosecondLocator.__call__` access the axes using the same helper method that the other date Locators use. - Make sure all the Date Locators return empty lists if the axes are empty instead of some raising `ValueError`s while others do not. - For the tests, `matplotlib.tests.test_dates:test_auto_date_locator` is still appropriate. All the Locators are tested in there. --- .../2015-04-12_microsecondlocator.rst | 17 ++++++ doc/users/whats_new/datelocators.rst | 15 +++++ lib/matplotlib/dates.py | 59 +++++++++++++------ 3 files changed, 73 insertions(+), 18 deletions(-) create mode 100644 doc/api/api_changes/2015-04-12_microsecondlocator.rst create mode 100644 doc/users/whats_new/datelocators.rst diff --git a/doc/api/api_changes/2015-04-12_microsecondlocator.rst b/doc/api/api_changes/2015-04-12_microsecondlocator.rst new file mode 100644 index 000000000000..d8366041c535 --- /dev/null +++ b/doc/api/api_changes/2015-04-12_microsecondlocator.rst @@ -0,0 +1,17 @@ +Removed `args` and `kwargs` from `MicrosecondLocator.__call__` +`````````````````````````````````````````````````````````````` + +The call signature of :meth:`~matplotlib.dates.MicrosecondLocator.__call__` +has changed from `__call__(self, *args, **kwargs)` to `__call__(self)`. +This is consistent with the super class :class:`~matplotlib.ticker.Locator` +and also all the other Locators derived from this super class. + + +No `ValueError` for the MicrosecondLocator and YearLocator +`````````````````````````````````````````````````````````` + +The :class:`~matplotlib.dates.MicrosecondLocator` and +:class:`~matplotlib.dates.YearLocator` objects when called will return +an empty list if the axes have no data or the view has no interval. +Previously, they raised a `ValueError`. This is consistent with all +the Date Locators. diff --git a/doc/users/whats_new/datelocators.rst b/doc/users/whats_new/datelocators.rst new file mode 100644 index 000000000000..b894a323587d --- /dev/null +++ b/doc/users/whats_new/datelocators.rst @@ -0,0 +1,15 @@ +Date Locators +------------- + +Date Locators (derived from :class:`~matplotlib.dates.DateLocator`) now +implement the :meth:`~matplotlib.tickers.Locator.tick_values` method. +This is expected of all Locators derived from :class:`~matplotlib.tickers.Locator`. + +The Date Locators can now be used easily without creating axes + + from datetime import datetime + from matplotlib.dates import YearLocator + t0 = datetime(2002, 10, 9, 12, 10) + tf = datetime(2005, 10, 9, 12, 15) + loc = YearLocator() + values = loc.tick_values(t0, tf) diff --git a/lib/matplotlib/dates.py b/lib/matplotlib/dates.py index e3f480a0c139..33299be854e4 100755 --- a/lib/matplotlib/dates.py +++ b/lib/matplotlib/dates.py @@ -667,18 +667,21 @@ def __call__(self): except ValueError: return [] - if dmin > dmax: - dmax, dmin = dmin, dmax - delta = relativedelta(dmax, dmin) + return self.tick_values(dmin, dmax) + + def tick_values(self, vmin, vmax): + if vmin > vmax: + vmax, vmin = vmin, vmax + delta = relativedelta(vmax, vmin) # We need to cap at the endpoints of valid datetime try: - start = dmin - delta + start = vmin - delta except ValueError: start = _from_ordinalf(1.0) try: - stop = dmax + delta + stop = vmax + delta except ValueError: # The magic number! stop = _from_ordinalf(3652059.9999999) @@ -688,19 +691,19 @@ def __call__(self): # estimate the number of ticks very approximately so we don't # have to do a very expensive (and potentially near infinite) # 'between' calculation, only to find out it will fail. - nmax, nmin = date2num((dmax, dmin)) + nmax, nmin = date2num((vmax, vmin)) estimate = (nmax - nmin) / (self._get_unit() * self._get_interval()) # This estimate is only an estimate, so be really conservative # about bailing... if estimate > self.MAXTICKS * 2: raise RuntimeError( 'RRuleLocator estimated to generate %d ticks from %s to %s: ' - 'exceeds Locator.MAXTICKS * 2 (%d) ' % (estimate, dmin, dmax, + 'exceeds Locator.MAXTICKS * 2 (%d) ' % (estimate, vmin, vmax, self.MAXTICKS * 2)) - dates = self.rule.between(dmin, dmax, True) + dates = self.rule.between(vmin, vmax, True) if len(dates) == 0: - return date2num([dmin, dmax]) + return date2num([vmin, vmax]) return self.raise_if_exceeds(date2num(dates)) def _get_unit(self): @@ -866,6 +869,9 @@ def __call__(self): self.refresh() return self._locator() + def tick_values(self, vmin, vmax): + return self.get_locator(vmin, vmax).tick_values(vmin, vmax) + def nonsingular(self, vmin, vmax): # whatever is thrown at us, we can scale the unit. # But default nonsingular date plots at an ~4 year period. @@ -1012,11 +1018,19 @@ def __init__(self, base=1, month=1, day=1, tz=None): } def __call__(self): - dmin, dmax = self.viewlim_to_dt() - ymin = self.base.le(dmin.year) - ymax = self.base.ge(dmax.year) + # if no data have been set, this will tank with a ValueError + try: + dmin, dmax = self.viewlim_to_dt() + except ValueError: + return [] + + return self.tick_values(dmin, dmax) + + def tick_values(self, vmin, vmax): + ymin = self.base.le(vmin.year) + ymax = self.base.ge(vmax.year) - ticks = [dmin.replace(year=ymin, **self.replaced)] + ticks = [vmin.replace(year=ymin, **self.replaced)] while 1: dt = ticks[-1] if dt.year >= ymax: @@ -1184,11 +1198,20 @@ def set_data_interval(self, vmin, vmax): self._wrapped_locator.set_data_interval(vmin, vmax) return DateLocator.set_data_interval(self, vmin, vmax) - def __call__(self, *args, **kwargs): - vmin, vmax = self.axis.get_view_interval() - vmin *= MUSECONDS_PER_DAY - vmax *= MUSECONDS_PER_DAY - ticks = self._wrapped_locator.tick_values(vmin, vmax) + def __call__(self): + # if no data have been set, this will tank with a ValueError + try: + dmin, dmax = self.viewlim_to_dt() + except ValueError: + return [] + + return self.tick_values(dmin, dmax) + + def tick_values(self, vmin, vmax): + nmin, nmax = date2num((vmin, vmax)) + nmin *= MUSECONDS_PER_DAY + nmax *= MUSECONDS_PER_DAY + ticks = self._wrapped_locator.tick_values(nmin, nmax) ticks = [tick / MUSECONDS_PER_DAY for tick in ticks] return ticks