Skip to content

Commit cf0116b

Browse files
authored
Merge pull request #26969 from jklymak/doc-units
DOC: add units to user/explain [ci doc]
2 parents 7c02e85 + b420c9d commit cf0116b

File tree

5 files changed

+300
-1
lines changed

5 files changed

+300
-1
lines changed

galleries/examples/ticks/date_concise_formatter.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
"""
2+
.. _date_concise_formatter:
3+
24
================================================
35
Formatting date ticks using ConciseDateFormatter
46
================================================

galleries/examples/ticks/date_formatters_locators.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
"""
2+
.. _date_formatters_locators:
3+
24
=================================
35
Date tick locators and formatters
46
=================================

galleries/examples/units/basic_units.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
"""
2+
.. _basic_units:
3+
24
===========
35
Basic Units
46
===========
Lines changed: 293 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,293 @@
1+
"""
2+
.. _user_axes_units:
3+
4+
==========================
5+
Plotting dates and strings
6+
==========================
7+
8+
The most basic way to use Matplotlib plotting methods is to pass coordinates in
9+
as numerical numpy arrays. For example, ``plot(x, y)`` will work if ``x`` and
10+
``y`` are numpy arrays of floats (or integers). Plotting methods will also
11+
work if `numpy.asarray` will convert ``x`` and ``y`` to an array of floating
12+
point numbers; e.g. ``x`` could be a python list.
13+
14+
Matplotlib also has the ability to convert other data types if a "unit
15+
converter" exists for the data type. Matplotlib has two built-in converters,
16+
one for dates and the other for lists of strings. Other downstream libraries
17+
have their own converters to handle their data types.
18+
19+
The method to add converters to Matplotlib is described in `matplotlib.units`.
20+
Here we briefly overview the built-in date and string converters.
21+
22+
Date conversion
23+
===============
24+
25+
If ``x`` and/or ``y`` are a list of `datetime` or an array of
26+
`numpy.datetime64`, Matplotlib has a built-in converter that will convert the
27+
datetime to a float, and add tick locators and formatters to the axis that are
28+
appropriate for dates. See `matplotlib.dates`.
29+
30+
In the following example, the x-axis gains a converter that converts from
31+
`numpy.datetime64` to float, and a locator that put ticks at the beginning of
32+
the month, and a formatter that label the ticks appropriately:
33+
"""
34+
35+
import numpy as np
36+
37+
import matplotlib.dates as mdates
38+
import matplotlib.units as munits
39+
40+
import matplotlib.pyplot as plt
41+
42+
fig, ax = plt.subplots(figsize=(5.4, 2), layout='constrained')
43+
time = np.arange('1980-01-01', '1980-06-25', dtype='datetime64[D]')
44+
x = np.arange(len(time))
45+
ax.plot(time, x)
46+
47+
# %%
48+
#
49+
# Note that if we try to plot a float on the x-axis, it will be plotted in
50+
# units of days since the "epoch" for the converter, in this case 1970-01-01
51+
# (see :ref:`date-format`). So when we plot the value 0, the ticks start at
52+
# 1970-01-01. (The locator also now chooses every two years for a tick instead
53+
# of every month):
54+
55+
fig, ax = plt.subplots(figsize=(5.4, 2), layout='constrained')
56+
time = np.arange('1980-01-01', '1980-06-25', dtype='datetime64[D]')
57+
x = np.arange(len(time))
58+
ax.plot(time, x)
59+
# 0 gets labeled as 1970-01-01
60+
ax.plot(0, 0, 'd')
61+
ax.text(0, 0, ' Float x=0', rotation=45)
62+
63+
# %%
64+
#
65+
# We can customize the locator and the formatter; see :ref:`date-locators` and
66+
# :ref:`date-formatters` for a complete list, and
67+
# :ref:`date_formatters_locators` for examples of them in use. Here we locate
68+
# by every second month, and format just with the month's 3-letter name using
69+
# ``"%b"`` (see `~datetime.datetime.strftime` for format codes):
70+
71+
fig, ax = plt.subplots(figsize=(5.4, 2), layout='constrained')
72+
time = np.arange('1980-01-01', '1980-06-25', dtype='datetime64[D]')
73+
x = np.arange(len(time))
74+
ax.plot(time, x)
75+
ax.xaxis.set_major_locator(mdates.MonthLocator(bymonth=np.arange(1, 13, 2)))
76+
ax.xaxis.set_major_formatter(mdates.DateFormatter('%b'))
77+
ax.set_xlabel('1980')
78+
79+
# %%
80+
#
81+
# The default locator is the `~.dates.AutoDateLocator`, and the default
82+
# Formatter `~.dates.AutoDateFormatter`. There are also "concise" formatter
83+
# and locators that give a more compact labelling, and can be set via rcParams.
84+
# Note how instead of the redundant "Jan" label at the start of the year,
85+
# "1980" is used instead. See :ref:`date_concise_formatter` for more examples.
86+
87+
plt.rcParams['date.converter'] = 'concise'
88+
89+
fig, ax = plt.subplots(figsize=(5.4, 2), layout='constrained')
90+
time = np.arange('1980-01-01', '1980-06-25', dtype='datetime64[D]')
91+
x = np.arange(len(time))
92+
ax.plot(time, x)
93+
94+
# %%
95+
#
96+
# We can set the limits on the axis either by passing the appropriate dates as
97+
# limits, or by passing a floating-point value in the proper units of days
98+
# since the epoch. If we need it, we can get this value from
99+
# `~.dates.date2num`.
100+
101+
fig, axs = plt.subplots(2, 1, figsize=(5.4, 3), layout='constrained')
102+
for ax in axs.flat:
103+
time = np.arange('1980-01-01', '1980-06-25', dtype='datetime64[D]')
104+
x = np.arange(len(time))
105+
ax.plot(time, x)
106+
107+
# set xlim using datetime64:
108+
axs[0].set_xlim(np.datetime64('1980-02-01'), np.datetime64('1980-04-01'))
109+
110+
# set xlim using floats:
111+
# Note can get from mdates.date2num(np.datetime64('1980-02-01'))
112+
axs[1].set_xlim(3683, 3683+60)
113+
114+
# %%
115+
#
116+
# String conversion: categorical plots
117+
# ====================================
118+
#
119+
# Sometimes we want to label categories on an axis rather than numbers.
120+
# Matplotlib allows this using a "categorical" converter (see
121+
# `~.matplotlib.category`).
122+
123+
data = {'apple': 10, 'orange': 15, 'lemon': 5, 'lime': 20}
124+
names = list(data.keys())
125+
values = list(data.values())
126+
127+
fig, axs = plt.subplots(1, 3, figsize=(7, 3), sharey=True, layout='constrained')
128+
axs[0].bar(names, values)
129+
axs[1].scatter(names, values)
130+
axs[2].plot(names, values)
131+
fig.suptitle('Categorical Plotting')
132+
133+
# %%
134+
#
135+
# Note that the "categories" are plotted in the order that they are first
136+
# specified and that subsequent plotting in a different order will not affect
137+
# the original order. Further, new additions will be added on the end (see
138+
# "pear" below):
139+
140+
fig, ax = plt.subplots(figsize=(5, 3), layout='constrained')
141+
ax.bar(names, values)
142+
143+
# plot in a different order:
144+
ax.scatter(['lemon', 'apple'], [7, 12])
145+
146+
# add a new category, "pear", and put the other categories in a different order:
147+
ax.plot(['pear', 'orange', 'apple', 'lemon'], [13, 10, 7, 12], color='C1')
148+
149+
150+
# %%
151+
#
152+
# Note that when using ``plot`` like in the above, the order of the plotting is
153+
# mapped onto the original order of the data, so the new line goes in the order
154+
# specified.
155+
#
156+
# The category converter maps from categories to integers, starting at zero. So
157+
# data can also be manually added to the axis using a float. Note that if a
158+
# float is passed in that does not have a "category" associated with it, the
159+
# data point can still be plotted, but a tick will not be created. In the
160+
# following, we plot data at 4.0 and 2.5, but no tick is added there because
161+
# those are not categories.
162+
163+
fig, ax = plt.subplots(figsize=(5, 3), layout='constrained')
164+
ax.bar(names, values)
165+
# arguments for styling the labels below:
166+
args = {'rotation': 70, 'color': 'C1',
167+
'bbox': {'color': 'white', 'alpha': .7, 'boxstyle': 'round'}}
168+
169+
170+
# 0 gets labeled as "apple"
171+
ax.plot(0, 2, 'd', color='C1')
172+
ax.text(0, 3, 'Float x=0', **args)
173+
174+
# 2 gets labeled as "lemon"
175+
ax.plot(2, 2, 'd', color='C1')
176+
ax.text(2, 3, 'Float x=2', **args)
177+
178+
# 4 doesn't get a label
179+
ax.plot(4, 2, 'd', color='C1')
180+
ax.text(4, 3, 'Float x=4', **args)
181+
182+
# 2.5 doesn't get a label
183+
ax.plot(2.5, 2, 'd', color='C1')
184+
ax.text(2.5, 3, 'Float x=2.5', **args)
185+
186+
# %%
187+
#
188+
# Setting the limits for a category axis can be done by specifying the
189+
# categories, or by specifying floating point numbers:
190+
191+
fig, axs = plt.subplots(2, 1, figsize=(5, 5), layout='constrained')
192+
ax = axs[0]
193+
ax.bar(names, values)
194+
ax.set_xlim('orange', 'lemon')
195+
ax.set_xlabel('limits set with categories')
196+
ax = axs[1]
197+
ax.bar(names, values)
198+
ax.set_xlim(0.5, 2.5)
199+
ax.set_xlabel('limits set with floats')
200+
201+
# %%
202+
#
203+
# The category axes are helpful for some plot types, but can lead to confusion
204+
# if data is read in as a list of strings, even if it is meant to be a list of
205+
# floats or dates. This sometimes happens when reading comma-separated value
206+
# (CSV) files. The categorical locator and formatter will put a tick at every
207+
# string value and label each one as well:
208+
209+
fig, ax = plt.subplots(figsize=(5.4, 2.5), layout='constrained')
210+
x = [str(xx) for xx in np.arange(100)] # list of strings
211+
ax.plot(x, np.arange(100))
212+
ax.set_xlabel('x is list of strings')
213+
214+
# %%
215+
#
216+
# If this is not desired, then simply convert the data to floats before plotting:
217+
218+
fig, ax = plt.subplots(figsize=(5.4, 2.5), layout='constrained')
219+
x = np.asarray(x, dtype='float') # array of float.
220+
ax.plot(x, np.arange(100))
221+
ax.set_xlabel('x is array of floats')
222+
223+
# %%
224+
#
225+
# Determine converter, formatter, and locator on an axis
226+
# ======================================================
227+
#
228+
# Sometimes it is helpful to be able to debug what Matplotlib is using to
229+
# convert the incoming data. We can do that by querying the ``converter``
230+
# property on the axis. We can also query the formatters and locators using
231+
# `~.axis.Axis.get_major_locator` and `~.axis.Axis.get_major_formatter`.
232+
#
233+
# Note that by default the converter is *None*.
234+
235+
fig, axs = plt.subplots(3, 1, figsize=(6.4, 7), layout='constrained')
236+
x = np.arange(100)
237+
ax = axs[0]
238+
ax.plot(x, x)
239+
label = f'Converter: {ax.xaxis.converter}\n '
240+
label += f'Locator: {ax.xaxis.get_major_locator()}\n'
241+
label += f'Formatter: {ax.xaxis.get_major_formatter()}\n'
242+
ax.set_xlabel(label)
243+
244+
ax = axs[1]
245+
time = np.arange('1980-01-01', '1980-06-25', dtype='datetime64[D]')
246+
x = np.arange(len(time))
247+
ax.plot(time, x)
248+
label = f'Converter: {ax.xaxis.converter}\n '
249+
label += f'Locator: {ax.xaxis.get_major_locator()}\n'
250+
label += f'Formatter: {ax.xaxis.get_major_formatter()}\n'
251+
ax.set_xlabel(label)
252+
253+
ax = axs[2]
254+
data = {'apple': 10, 'orange': 15, 'lemon': 5, 'lime': 20}
255+
names = list(data.keys())
256+
values = list(data.values())
257+
ax.plot(names, values)
258+
label = f'Converter: {ax.xaxis.converter}\n '
259+
label += f'Locator: {ax.xaxis.get_major_locator()}\n'
260+
label += f'Formatter: {ax.xaxis.get_major_formatter()}\n'
261+
ax.set_xlabel(label)
262+
263+
# %%
264+
#
265+
# More about "unit" support
266+
# =========================
267+
#
268+
# The support for dates and categories is part of "units" support that is built
269+
# into Matplotlib. This is described at `.matplotlib.units` and in the
270+
# :ref:`basic_units` example.
271+
#
272+
# Unit support works by querying the type of data passed to the plotting
273+
# function and dispatching to the first converter in a list that accepts that
274+
# type of data. So below, if ``x`` has ``datetime`` objects in it, the
275+
# converter will be ``_SwitchableDateConverter``; if it has has strings in it,
276+
# it will be sent to the ``StrCategoryConverter``.
277+
278+
for k, v in munits.registry.items():
279+
print(f"type: {k};\n converter: {type(v)}")
280+
281+
# %%
282+
#
283+
# There are a number of downstream libraries that provide their own converters
284+
# with locators and formatters. Physical unit support is provided by
285+
# `astropy <https://www.astropy.org>`_, `pint <https://pint.readthedocs.io>`_, and
286+
# `unyt <https://unyt.readthedocs.io>`_, among others.
287+
#
288+
# High level libraries like `pandas <https://pandas.pydata.org>`_ and
289+
# `nc-time-axis <https://nc-time-axis.readthedocs.io>`_ (and thus
290+
# `xarray <https://docs.xarray.dev>`_) provide their own datetime support.
291+
# This support can sometimes be incompatible with Matplotlib native datetime
292+
# support, so care should be taken when using Matplotlib locators and
293+
# formatters if these libraries are being used.

galleries/users_explain/axes/index.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ annotations like x- and y-labels, titles, and legends.
2424
color='darkgrey')
2525
fig.suptitle('plt.subplots()')
2626

27-
2827
.. toctree::
2928
:maxdepth: 2
3029

@@ -43,6 +42,7 @@ annotations like x- and y-labels, titles, and legends.
4342

4443
axes_scales
4544
axes_ticks
45+
axes_units
4646
Legends <legend_guide>
4747
Subplot mosaic <mosaic>
4848

0 commit comments

Comments
 (0)