Skip to content

DOC: Update and consolidate Custom Tick Formatter for Time Series example #21873

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jan 5, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions examples/lines_bars_and_markers/scatter_demo2.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,10 @@
import matplotlib.pyplot as plt
import matplotlib.cbook as cbook

# Load a numpy record array from yahoo csv data with fields date, open, close,
# volume, adj_close from the mpl-data/example directory. The record array
# stores the date as an np.datetime64 with a day unit ('D') in the date column.
# Load a numpy record array from yahoo csv data with fields date, open, high,
# low, close, volume, adj_close from the mpl-data/sample_data directory. The
# record array stores the date as an np.datetime64 with a day unit ('D') in
# the date column.
price_data = (cbook.get_sample_data('goog.npz', np_load=True)['price_data']
.view(np.recarray))
price_data = price_data[-250:] # get the most recent 250 trading days
Expand Down
8 changes: 4 additions & 4 deletions examples/text_labels_and_annotations/date.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,10 @@
import matplotlib.dates as mdates
import matplotlib.cbook as cbook

# Load a numpy structured array from yahoo csv data with fields date, open,
# close, volume, adj_close from the mpl-data/example directory. This array
# stores the date as an np.datetime64 with a day unit ('D') in the 'date'
# column.
# Load a numpy record array from yahoo csv data with fields date, open, high,
# low, close, volume, adj_close from the mpl-data/sample_data directory. The
# record array stores the date as an np.datetime64 with a day unit ('D') in
# the date column.
data = cbook.get_sample_data('goog.npz', np_load=True)['price_data']

fig, axs = plt.subplots(3, 1, figsize=(6.4, 7), constrained_layout=True)
Expand Down
56 changes: 0 additions & 56 deletions examples/text_labels_and_annotations/date_index_formatter.py

This file was deleted.

83 changes: 83 additions & 0 deletions examples/ticks/date_index_formatter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
"""
=====================================
Custom tick formatter for time series
=====================================

.. redirect-from:: /gallery/text_labels_and_annotations/date_index_formatter
.. redirect-from:: /gallery/ticks/date_index_formatter2

When plotting daily data, e.g., financial time series, one often wants
to leave out days on which there is no data, for instance weekends, so that
the data are plotted at regular intervals without extra spaces for the days
with no data.
The example shows how to use an 'index formatter' to achieve the desired plot.
"""

import numpy as np
import matplotlib.pyplot as plt
import matplotlib.cbook as cbook
import matplotlib.lines as ml
from matplotlib.dates import DateFormatter, DayLocator
from matplotlib.ticker import Formatter


# Load a numpy record array from yahoo csv data with fields date, open, high,
# low, close, volume, adj_close from the mpl-data/sample_data directory. The
# record array stores the date as an np.datetime64 with a day unit ('D') in
# the date column (``r.date``).
r = (cbook.get_sample_data('goog.npz', np_load=True)['price_data']
.view(np.recarray))
r = r[:9] # get the first 9 days

fig, (ax1, ax2) = plt.subplots(nrows=2, figsize=(6, 6),
constrained_layout={'hspace': .15})

# First we'll do it the default way, with gaps on weekends
ax1.plot(r.date, r.adj_close, 'o-')

# Highlight gaps in daily data
gaps = np.flatnonzero(np.diff(r.date) > np.timedelta64(1, 'D'))
for gap in r[['date', 'adj_close']][np.stack((gaps, gaps + 1)).T]:
ax1.plot(gap.date, gap.adj_close, 'w--', lw=2)
ax1.legend(handles=[ml.Line2D([], [], ls='--', label='Gaps in daily data')])

ax1.set_title("Plot y at x Coordinates")
ax1.xaxis.set_major_locator(DayLocator())
ax1.xaxis.set_major_formatter(DateFormatter('%a'))


# Next we'll write a custom index formatter. Below we will plot
# the data against an index that goes from 0, 1, ... len(data). Instead of
# formatting the tick marks as integers, we format as times.
def format_date(x, _):
try:
# convert datetime64 to datetime, and use datetime's strftime:
return r.date[round(x)].item().strftime('%a')
except IndexError:
pass

# Create an index plot (x defaults to range(len(y)) if omitted)
ax2.plot(r.adj_close, 'o-')
Comment on lines +59 to +60
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe just be explicit rather than make the user go back and parse the comment?

Suggested change
# Create an index plot (x defaults to range(len(y)) if omitted)
ax2.plot(r.adj_close, 'o-')
# Create an index plot:
ax2.plot(np.arange(0, len(r.adj_close)), r.adj_close, 'o-')

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I made this change on purpose to underline/explain that this is an index plot which by definition plots the values versus an index number. In my view, the plot command is more legible/clearer without the range definition as the first argument. Besides, this kind of plot is explicitly advertised in plot, so I'd rather keep it as is.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think, however, that most of the core devs consider this a nasty holdover from the Matlab days, and prefer the more explicit interface. Certainly when I was reading this I was confused about what r.adj_close was, and how it was plotting x and y, and then I had to go up, figure out what r.adj_close was and then figure out why it didn't need two arguments.

In general, I wonder if it is more confusing than not to keep r as a structured array? How common do we think those are in the wild (I never use them, but then I use xarray all the time).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

most of the core devs consider this a nasty holdover from the Matlab days, and prefer the more explicit interface

hm, I find this feature quite handy, but if this usage is not recommended it should be described as discouraged in the docs, shouldn't it

I wonder if it is more confusing than not to keep r as a structured array

Alternatives (pandas, xarray, ...) require additional dependencies, csv requires additional date conversion, so why not leave it as a structured array (given that the data structure is described in detail in the comment at lines 24-27)?


ax2.set_title("Plot y at Index Coordinates Using Custom Formatter")
ax2.xaxis.set_major_formatter(format_date) # internally creates FuncFormatter

#############################################################################
# Instead of passing a function into `.Axis.set_major_formatter` you can use
# any other callable, e.g. an instance of a class that implements __call__:


class MyFormatter(Formatter):
def __init__(self, dates, fmt='%a'):
self.dates = dates
self.fmt = fmt

def __call__(self, x, pos=0):
"""Return the label for time x at position pos."""
try:
return self.dates[round(x)].item().strftime(self.fmt)
except IndexError:
pass


ax2.xaxis.set_major_formatter(MyFormatter(r.date, '%a'))
45 changes: 0 additions & 45 deletions examples/ticks/date_index_formatter2.py

This file was deleted.