|
| 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. |
0 commit comments