From a76c70f1dd87a7a9547b0cb012de03104e597aa4 Mon Sep 17 00:00:00 2001 From: Jody Klymak Date: Tue, 10 Jul 2018 10:58:25 -0700 Subject: [PATCH] ENH: New ConciseDateFormatter formatter for dates FIX: allow non-unit aware tickers FIX: make str list length more strict DOC FIX: add kwargs to ConciseDateConverter FIX: fix cursor formatter FIX: cleanup loop logic FIX: cleanup append --- .flake8 | 1 + .../next_whats_new/concise_date_formatter.rst | 40 +++ .../date_concise_formatter.py | 183 +++++++++++ lib/matplotlib/dates.py | 293 +++++++++++++++++- lib/matplotlib/tests/test_dates.py | 51 +++ tutorials/text/text_intro.py | 17 +- 6 files changed, 559 insertions(+), 26 deletions(-) create mode 100644 doc/users/next_whats_new/concise_date_formatter.rst create mode 100644 examples/ticks_and_spines/date_concise_formatter.py diff --git a/.flake8 b/.flake8 index 4cb738b4b544..b780e976cb19 100644 --- a/.flake8 +++ b/.flake8 @@ -240,6 +240,7 @@ per-file-ignores = examples/text_labels_and_annotations/tex_demo.py: E402 examples/text_labels_and_annotations/watermark_text.py: E402 examples/ticks_and_spines/auto_ticks.py: E501 + examples/ticks_and_spines/date_concise_formatter.py: E402 examples/user_interfaces/canvasagg.py: E402 examples/user_interfaces/embedding_in_gtk3_panzoom_sgskip.py: E402 examples/user_interfaces/embedding_in_gtk3_sgskip.py: E402 diff --git a/doc/users/next_whats_new/concise_date_formatter.rst b/doc/users/next_whats_new/concise_date_formatter.rst new file mode 100644 index 000000000000..cb9021be6921 --- /dev/null +++ b/doc/users/next_whats_new/concise_date_formatter.rst @@ -0,0 +1,40 @@ +:orphan: + +New date formatter: `~.dates.ConciseDateFormatter` +-------------------------------------------------- + +The automatic date formatter used by default can be quite verbose. A new +formatter can be accessed that tries to make the tick labels appropriately +concise. + + .. plot:: + + import datetime + import matplotlib.pyplot as plt + import matplotlib.dates as mdates + import numpy as np + + # make a timeseries... + base = datetime.datetime(2005, 2, 1) + dates = np.array([base + datetime.timedelta(hours= 2 * i) + for i in range(732)]) + N = len(dates) + np.random.seed(19680801) + y = np.cumsum(np.random.randn(N)) + + lims = [(np.datetime64('2005-02'), np.datetime64('2005-04')), + (np.datetime64('2005-02-03'), np.datetime64('2005-02-15')), + (np.datetime64('2005-02-03 11:00'), np.datetime64('2005-02-04 13:20'))] + fig, axs = plt.subplots(3, 1, constrained_layout=True) + for nn, ax in enumerate(axs): + # activate the formatter here. + locator = mdates.AutoDateLocator() + formatter = mdates.ConciseDateFormatter(locator) + ax.xaxis.set_major_locator(locator) + ax.xaxis.set_major_formatter(formatter) + + ax.plot(dates, y) + ax.set_xlim(lims[nn]) + axs[0].set_title('Concise Date Formatter') + + plt.show() diff --git a/examples/ticks_and_spines/date_concise_formatter.py b/examples/ticks_and_spines/date_concise_formatter.py new file mode 100644 index 000000000000..a672d80a0139 --- /dev/null +++ b/examples/ticks_and_spines/date_concise_formatter.py @@ -0,0 +1,183 @@ +""" +================================================ +Formatting date ticks using ConciseDateFormatter +================================================ + +Finding good tick values and formatting the ticks for an axis that +has date data is often a challenge. `~.dates.ConciseDateFormatter` is +meant to improve the strings chosen for the ticklabels, and to minimize +the strings used in those tick labels as much as possible. + +.. note:: + + This formatter is a candidate to become the default date tick formatter + in future versions of Matplotlib. Please report any issues or + suggestions for improvement to the github repository or mailing list. + +""" +import datetime +import matplotlib.pyplot as plt +import matplotlib.dates as mdates +import numpy as np + +############################################################################# +# First, the default formatter. + +base = datetime.datetime(2005, 2, 1) +dates = np.array([base + datetime.timedelta(hours=(2 * i)) + for i in range(732)]) +N = len(dates) +np.random.seed(19680801) +y = np.cumsum(np.random.randn(N)) + +fig, axs = plt.subplots(3, 1, constrained_layout=True, figsize=(6, 6)) +lims = [(np.datetime64('2005-02'), np.datetime64('2005-04')), + (np.datetime64('2005-02-03'), np.datetime64('2005-02-15')), + (np.datetime64('2005-02-03 11:00'), np.datetime64('2005-02-04 13:20'))] +for nn, ax in enumerate(axs): + ax.plot(dates, y) + ax.set_xlim(lims[nn]) + # rotate_labels... + for label in ax.get_xticklabels(): + label.set_rotation(40) + label.set_horizontalalignment('right') +axs[0].set_title('Default Date Formatter') +plt.show() + +############################################################################# +# The default date formater is quite verbose, so we have the option of +# using `~.dates.ConciseDateFormatter`, as shown below. Note that +# for this example the labels do not need to be rotated as they do for the +# default formatter because the labels are as small as possible. + +fig, axs = plt.subplots(3, 1, constrained_layout=True, figsize=(6, 6)) +for nn, ax in enumerate(axs): + locator = mdates.AutoDateLocator(minticks=3, maxticks=7) + formatter = mdates.ConciseDateFormatter(locator) + ax.xaxis.set_major_locator(locator) + ax.xaxis.set_major_formatter(formatter) + + ax.plot(dates, y) + ax.set_xlim(lims[nn]) +axs[0].set_title('Concise Date Formatter') + +plt.show() + +############################################################################# +# If all calls to axes that have dates are to be made using this converter, +# it is probably most convenient to use the units registry where you do +# imports: + +import matplotlib.units as munits +converter = mdates.ConciseDateConverter() +munits.registry[np.datetime64] = converter +munits.registry[datetime.date] = converter +munits.registry[datetime.datetime] = converter + +fig, axs = plt.subplots(3, 1, figsize=(6, 6), constrained_layout=True) +for nn, ax in enumerate(axs): + ax.plot(dates, y) + ax.set_xlim(lims[nn]) +axs[0].set_title('Concise Date Formatter') + +plt.show() + +############################################################################# +# Localization of date formats +# ============================ +# +# Dates formats can be localized if the default formats are not desirable by +# manipulating one of three lists of strings. +# +# The ``formatter.formats`` list of formats is for the normal tick labels, +# There are six levels: years, months, days, hours, minutes, seconds. +# The ``formatter.offset_formats`` is how the "offset" string on the right +# of the axis is formatted. This is usually much more verbose than the tick +# labels. Finally, the ``formatter.zero_formats`` are the formats of the +# ticks that are "zeros". These are tick values that are either the first of +# the year, month, or day of month, or the zeroth hour, minute, or second. +# These are usually the same as the format of +# the ticks a level above. For example if the axis limts mean the ticks are +# mostly days, then we label 1 Mar 2005 simply with a "Mar". If the axis +# limits are mostly hours, we label Feb 4 00:00 as simply "Feb-4". +# +# Note that these format lists can also be passed to `.ConciseDateFormatter` +# as optional kwargs. +# +# Here we modify the labels to be "day month year", instead of the ISO +# "year month day": + +fig, axs = plt.subplots(3, 1, constrained_layout=True, figsize=(6, 6)) + +for nn, ax in enumerate(axs): + locator = mdates.AutoDateLocator() + formatter = mdates.ConciseDateFormatter(locator) + formatter.formats = ['%y', # ticks are mostly years + '%b', # ticks are mostly months + '%d', # ticks are mostly days + '%H:%M', # hrs + '%H:%M', # min + '%S.%f', ] # secs + # these are mostly just the level above... + formatter.zero_formats = [''] + formatter.formats[:-1] + # ...except for ticks that are mostly hours, then it is nice to have + # month-day: + formatter.zero_formats[3] = '%d-%b' + + formatter.offset_formats = ['', + '%Y', + '%b %Y', + '%d %b %Y', + '%d %b %Y', + '%d %b %Y %H:%M', ] + ax.xaxis.set_major_locator(locator) + ax.xaxis.set_major_formatter(formatter) + + ax.plot(dates, y) + ax.set_xlim(lims[nn]) +axs[0].set_title('Concise Date Formatter') + +plt.show() + +############################################################################# +# Registering a converter with localization +# ========================================= +# +# `.ConciseDateFormatter` doesn't have rcParams entries, but localization +# can be accomplished by passing kwargs to `~.ConciseDateConverter` and +# registering the datatypes you will use with the units registry: + +import datetime + +formats = ['%y', # ticks are mostly years + '%b', # ticks are mostly months + '%d', # ticks are mostly days + '%H:%M', # hrs + '%H:%M', # min + '%S.%f', ] # secs +# these can be the same, except offset by one level.... +zero_formats = [''] + formats[:-1] +# ...except for ticks that are mostly hours, then its nice to have month-day +zero_formats[3] = '%d-%b' +offset_formats = ['', + '%Y', + '%b %Y', + '%d %b %Y', + '%d %b %Y', + '%d %b %Y %H:%M', ] + +converter = mdates.ConciseDateConverter(formats=formats, + zero_formats=zero_formats, + offset_formats=offset_formats) + +munits.registry[np.datetime64] = converter +munits.registry[datetime.date] = converter +munits.registry[datetime.datetime] = converter + +fig, axs = plt.subplots(3, 1, constrained_layout=True, figsize=(6, 6)) +for nn, ax in enumerate(axs): + ax.plot(dates, y) + ax.set_xlim(lims[nn]) +axs[0].set_title('Concise Date Formatter registered non-default') + +plt.show() diff --git a/lib/matplotlib/dates.py b/lib/matplotlib/dates.py index 339114fbb281..b55f526e758e 100644 --- a/lib/matplotlib/dates.py +++ b/lib/matplotlib/dates.py @@ -128,6 +128,11 @@ * :class:`AutoDateFormatter`: attempts to figure out the best format to use. This is most useful when used with the :class:`AutoDateLocator`. + * :class:`ConciseDateFormatter`: also attempts to figure out the best + format to use, and to make the format as compact as possible while + still having complete date information. This is most useful when used + with the :class:`AutoDateLocator`. + * :class:`DateFormatter`: use :func:`strftime` format strings * :class:`IndexDateFormatter`: date plots with implicit *x* @@ -158,8 +163,8 @@ __all__ = ('datestr2num', 'date2num', 'num2date', 'num2timedelta', 'drange', 'epoch2num', 'num2epoch', 'mx2num', 'DateFormatter', - 'IndexDateFormatter', 'AutoDateFormatter', 'DateLocator', - 'RRuleLocator', 'AutoDateLocator', 'YearLocator', + 'ConciseDateFormatter', 'IndexDateFormatter', 'AutoDateFormatter', + 'DateLocator', 'RRuleLocator', 'AutoDateLocator', 'YearLocator', 'MonthLocator', 'WeekdayLocator', 'DayLocator', 'HourLocator', 'MinuteLocator', 'SecondLocator', 'MicrosecondLocator', @@ -408,8 +413,9 @@ def date2num(d): # this unpacks pandas series or dataframes... d = d.values if not np.iterable(d): - if (isinstance(d, np.datetime64) or (isinstance(d, np.ndarray) and - np.issubdtype(d.dtype, np.datetime64))): + if (isinstance(d, np.datetime64) or + (isinstance(d, np.ndarray) and + np.issubdtype(d.dtype, np.datetime64))): return _dt64_to_ordinalf(d) return _to_ordinalf(d) @@ -569,7 +575,7 @@ def drange(dstart, dend, delta): f2 = date2num(dinterval_end) # new float-endpoint return np.linspace(f1, f2, num + 1) -### date tickers and formatters ### +## date tickers and formatters ### class DateFormatter(ticker.Formatter): @@ -738,12 +744,235 @@ def __call__(self, x, pos=0): return num2date(self.t[ind], self.tz).strftime(self.fmt) +class ConciseDateFormatter(ticker.Formatter): + """ + This class attempts to figure out the best format to use for the + date, and to make it as compact as possible, but still be complete. This is + most useful when used with the :class:`AutoDateLocator`:: + + + >>> locator = AutoDateLocator() + >>> formatter = ConciseDateFormatter(locator) + + Parameters + ---------- + + locator : `.ticker.Locator` + Locator that this axis is using. + + tz : string, optional + Passed to `.dates.date2num`. + + formats : list of 6 strings, optional + Format strings for 6 levels of tick labelling: mostly years, + months, days, hours, minutes, and seconds. Strings use + the same format codes as `strftime`. Default is + ``['%Y', '%b', '%d', '%H:%M', '%H:%M', '%S.%f']`` + + zero_formats : list of 6 strings, optional + Format strings for tick labels that are "zeros" for a given tick + level. For instance, if most ticks are months, ticks around 1 Jan 2005 + will be labeled "Dec", "2005", "Feb". The default is + ``['', '%Y', '%b', '%b-%d', '%H:%M', '%H:%M']`` + + offset_formats : list of 6 strings, optional + Format strings for the 6 levels that is applied to the "offset" + string found on the right side of an x-axis, or top of a y-axis. + Combined with the tick labels this should completely specify the + date. The default is:: + + ['', '%Y', '%Y-%b', '%Y-%b-%d', '%Y-%b-%d', '%Y-%b-%d %H:%M'] + + show_offset : bool + Whether to show the offset or not. Default is ``True``. + + Examples + -------- + + See :doc:`/gallery/ticks_and_spines/date_concise_formatter` + + .. plot:: + + import datetime + import matplotlib.dates as mdates + + base = datetime.datetime(2005, 2, 1) + dates = np.array([base + datetime.timedelta(hours=(2 * i)) + for i in range(732)]) + N = len(dates) + np.random.seed(19680801) + y = np.cumsum(np.random.randn(N)) + + fig, ax = plt.subplots(constrained_layout=True) + locator = mdates.AutoDateLocator() + formatter = mdates.ConciseDateFormatter(locator) + ax.xaxis.set_major_locator(locator) + ax.xaxis.set_major_formatter(formatter) + + ax.plot(dates, y) + ax.set_title('Concise Date Formatter') + + """ + + def __init__(self, locator, tz=None, formats=None, offset_formats=None, + zero_formats=None, show_offset=True): + """ + Autoformat the date labels. The default format is used to form an + initial string, and then redundant elements are removed. + """ + self._locator = locator + self._tz = tz + self._oldticks = np.array([]) + self._oldlabels = None + self.defaultfmt = '%Y' + # there are 6 levels with each level getting a specific format + # 0: mostly years, 1: months, 2: days, + # 3: hours, 4: minutes, 5: seconds + if formats: + if len(formats) != 6: + raise ValueError('formats argument must be a list of ' + '6 format strings (or None)') + self.formats = formats + else: + self.formats = ['%Y', # ticks are mostly years + '%b', # ticks are mostly months + '%d', # ticks are mostly days + '%H:%M', # hrs + '%H:%M', # min + '%S.%f', # secs + ] + # fmt for zeros ticks at this level. These are + # ticks that should be labeled w/ info the level above. + # like 1 Jan can just be labled "Jan". 02:02:00 can + # just be labeled 02:02. + if zero_formats: + if len(formats) != 6: + raise ValueError('zero_formats argument must be a list of ' + '6 format strings (or None)') + self.zero_formats = zero_formats + elif formats: + # use the users formats for the zero tick formats + self.zero_formats = [''] + self.formats[:-1] + else: + # make the defaults a bit nicer: + self.zero_formats = [''] + self.formats[:-1] + self.zero_formats[3] = '%b-%d' + + if offset_formats: + if len(offset_formats) != 6: + raise ValueError('offsetfmts argument must be a list of ' + '6 format strings (or None)') + self.offset_formats = offset_formats + else: + self.offset_formats = ['', + '%Y', + '%Y-%b', + '%Y-%b-%d', + '%Y-%b-%d', + '%Y-%b-%d %H:%M'] + self.offset_string = '' + self._formatter = DateFormatter(self.defaultfmt, self._tz) + self.show_offset = show_offset + + def __call__(self, x, pos=None): + if hasattr(self._locator, '_get_unit'): + locator_unit_scale = float(self._locator._get_unit()) + else: + locator_unit_scale = 1.0 + ticks = self._locator() + if pos is not None: + if not np.array_equal(ticks, self._oldticks): + + offset_fmt = '' + fmt = self.defaultfmt + self._formatter = DateFormatter(fmt, self._tz) + tickdatetime = [num2date(tick) for tick in ticks] + tickdate = np.array([tdt.timetuple()[:6] + for tdt in tickdatetime]) + + # basic algorithm: + # 1) only display a part of the date if it changes over the + # ticks. + # 2) don't display the smaller part of the date if: + # it is always the same or if it is the start of the + # year, month, day etc. + # fmt for most ticks at this level + fmts = self.formats + # format beginnings of days, months, years, etc... + zerofmts = self.zero_formats + # offset fmt are for the offset in the upper left of the + # or lower right of the axis. + offsetfmts = self.offset_formats + + # determine the level we will label at: + # mostly 0: years, 1: months, 2: days, + # 3: hours, 4: minutes, 5: seconds, 6: microseconds + for level in range(5, -1, -1): + if len(np.unique(tickdate[:, level])) > 1: + break + + # level is the basic level we will label at. + # now loop through and decide the actual ticklabels + zerovals = [0, 1, 1, 0, 0, 0, 0] + ticknew = ['']*len(tickdate) + for nn in range(len(tickdate)): + if level < 5: + if tickdate[nn][level] == zerovals[level]: + fmt = zerofmts[level] + else: + fmt = fmts[level] + else: + # special handling for seconds + microseconds + if (tickdatetime[nn].second == 0 and + tickdatetime[nn].microsecond == 0): + fmt = zerofmts[level] + else: + fmt = fmts[level] + ticknew[nn] = tickdatetime[nn].strftime(fmt) + + # special handling of seconds and microseconds: + # strip extra zeros and decimal if possible... + # this is complicated by two factors. 1) we have some + # level-4 strings here (i.e. 03:00, '0.50000', '1.000') + # 2) we would like to have the same number of decimals for + # each string (i.e. 0.5 and 1.0). + if level >= 5: + trailing_zeros = min( + (len(s) - len(s.rstrip('0')) for s in ticknew + if '.' in s), + default=None) + if trailing_zeros: + for nn in range(len(ticknew)): + if '.' in ticknew[nn]: + ticknew[nn] = \ + ticknew[nn][:-trailing_zeros].rstrip('.') + + result = ticknew[pos] + self._oldticks = ticks + self._oldlabels = ticknew + + # set the offset string: + if self.show_offset: + self.offset_string = tickdatetime[-1].strftime( + offsetfmts[level]) + + result = self._oldlabels[pos] + else: + result = self._formatter(x, pos) + return result + + def get_offset(self): + return self.offset_string + + def format_data_short(self, value): + return num2date(value).strftime('%Y-%m-%d %H:%M:%S') + + class AutoDateFormatter(ticker.Formatter): """ This class attempts to figure out the best format to use. This is most useful when used with the :class:`AutoDateLocator`. - The AutoDateFormatter has a scale dictionary that maps the scale of the tick (the distance in days between one major tick) and a format string. The default looks like this:: @@ -1328,15 +1557,17 @@ def get_locator(self, dmin, dmax): self._freq = freq if self._byranges[i] and self.interval_multiples: - if i == DAILY and interval == 14: - # just make first and 15th. Avoids 30th. - byranges[i] = [1, 15] - else: - byranges[i] = self._byranges[i][::interval] + byranges[i] = self._byranges[i][::interval] + if i in (DAILY, WEEKLY): + if interval == 14: + # just make first and 15th. Avoids 30th. + byranges[i] = [1, 15] + elif interval == 7: + byranges[i] = [1, 8, 15, 22] + interval = 1 else: byranges[i] = self._byranges[i] - break else: raise ValueError('No sensible date limit could be found in the ' @@ -1825,6 +2056,44 @@ def default_units(x, axis): return None +class ConciseDateConverter(DateConverter): + """ + Converter for datetime.date and datetime.datetime data, + or for date/time data represented as it would be converted + by :func:`date2num`. + + The 'unit' tag for such data is None or a tzinfo instance. + """ + + def __init__(self, formats=None, zero_formats=None, offset_formats=None, + show_offset=True): + self._formats = formats + self._zero_formats = zero_formats + self._offset_formats = offset_formats + self._show_offset = show_offset + super().__init__() + + def axisinfo(self, unit, axis): + """ + Return the :class:`~matplotlib.units.AxisInfo` for *unit*. + + *unit* is a tzinfo instance or None. + The *axis* argument is required but not used. + """ + tz = unit + + majloc = AutoDateLocator(tz=tz) + majfmt = ConciseDateFormatter(majloc, tz=tz, formats=self._formats, + zero_formats=self._zero_formats, + offset_formats=self._offset_formats, + show_offset=self._show_offset) + datemin = datetime.date(2000, 1, 1) + datemax = datetime.date(2010, 1, 1) + + return units.AxisInfo(majloc=majloc, majfmt=majfmt, label='', + default_limits=(datemin, datemax)) + + units.registry[np.datetime64] = DateConverter() units.registry[datetime.date] = DateConverter() units.registry[datetime.datetime] = DateConverter() diff --git a/lib/matplotlib/tests/test_dates.py b/lib/matplotlib/tests/test_dates.py index 35e005346335..9365a51d37a2 100644 --- a/lib/matplotlib/tests/test_dates.py +++ b/lib/matplotlib/tests/test_dates.py @@ -506,6 +506,57 @@ def _create_auto_date_locator(date1, date2): assert list(map(str, mdates.num2date(locator()))) == expected +def test_concise_formatter(): + def _create_auto_date_locator(date1, date2): + fig, ax = plt.subplots() + + locator = mdates.AutoDateLocator(interval_multiples=True) + formatter = mdates.ConciseDateFormatter(locator) + ax.yaxis.set_major_locator(locator) + ax.yaxis.set_major_formatter(formatter) + ax.set_ylim(date1, date2) + fig.canvas.draw() + sts = [] + for st in ax.get_yticklabels(): + sts += [st.get_text()] + return sts + + d1 = datetime.datetime(1997, 1, 1) + results = ([datetime.timedelta(weeks=52 * 200), + [str(t) for t in range(1980, 2201, 20)] + ], + [datetime.timedelta(weeks=52), + ['1997', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', + 'Sep', 'Oct', 'Nov', 'Dec'] + ], + [datetime.timedelta(days=141), + ['Jan', '22', 'Feb', '22', 'Mar', '22', 'Apr', '22', + 'May', '22'] + ], + [datetime.timedelta(days=40), + ['Jan', '05', '09', '13', '17', '21', '25', '29', 'Feb', + '05', '09'] + ], + [datetime.timedelta(hours=40), + ['Jan-01', '04:00', '08:00', '12:00', '16:00', '20:00', + 'Jan-02', '04:00', '08:00', '12:00', '16:00'] + ], + [datetime.timedelta(minutes=20), + ['00:00', '00:05', '00:10', '00:15', '00:20'] + ], + [datetime.timedelta(seconds=40), + ['00:00', '05', '10', '15', '20', '25', '30', '35', '40'] + ], + [datetime.timedelta(seconds=2), + ['59.5', '00:00', '00.5', '01.0', '01.5', '02.0', '02.5'] + ], + ) + for t_delta, expected in results: + d2 = d1 + t_delta + strings = _create_auto_date_locator(d1, d2) + assert strings == expected + + @image_comparison(baseline_images=['date_inverted_limit'], extensions=['png']) def test_date_inverted_limit(): diff --git a/tutorials/text/text_intro.py b/tutorials/text/text_intro.py index 83fc0e2b782c..12f9b344478a 100644 --- a/tutorials/text/text_intro.py +++ b/tutorials/text/text_intro.py @@ -394,27 +394,16 @@ def formatoddticks(x, pos): ax.tick_params(axis='x', rotation=70) plt.show() -############################################################################## -# Maybe the format of the labels above is acceptable, but the choices is -# rather idiosyncratic. We can make the ticks fall on the start of the month -# by modifying `matplotlib.dates.AutoDateLocator` -import matplotlib.dates as mdates - -locator = mdates.AutoDateLocator(interval_multiples=True) - -fig, ax = plt.subplots(figsize=(5, 3), tight_layout=True) -ax.xaxis.set_major_locator(locator) -ax.plot(time, y1) -ax.tick_params(axis='x', rotation=70) -plt.show() ############################################################################## -# However, this changes the tick labels. The easiest fix is to pass a format +# We can pass a format # to `matplotlib.dates.DateFormatter`. Also note that the 29th and the # next month are very close together. We can fix this by using the # `dates.DayLocator` class, which allows us to specify a list of days of the # month to use. Similar formatters are listed in the `matplotlib.dates` module. +import matplotlib.dates as mdates + locator = mdates.DayLocator(bymonthday=[1, 15]) formatter = mdates.DateFormatter('%b %d')