|
| 1 | +""" |
| 2 | +.. _user_axes_units: |
| 3 | +
|
| 4 | +========================================= |
| 5 | +Plotting beyond floats: 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. |
57AE
tr>
| 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 locators and formatters to the axis that are |
| 28 | +appropriate for 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, and note that the locator now chooses every two years for a tick |
| 53 | +# instead 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 | +# |
| 66 | +# We can customize the locator and the formatter; see :ref:`date-locators` and |
| 67 | +# :ref:`date-formatters` for a complete list, and |
| 68 | +# :ref:`date_formatters_locators` for examples of them in use. Here we locate |
| 69 | +# by every second month, and format just with the month's 3-letter name using |
| 70 | +# ``"%b"`` (see `~datetime.datetime.strftime` for format codes): |
| 71 | + |
| 72 | +fig, ax = plt.subplots(figsize=(5.4, 2), layout='constrained') |
| 73 | +time = np.arange('1980-01-01', '1980-06-25', dtype='datetime64[D]') |
| 74 | +x = np.arange(len(time)) |
| 75 | +ax.plot(time, x) |
| 76 | +ax.xaxis.set_major_locator(mdates.MonthLocator(bymonth=np.arange(1, 13, 2))) |
| 77 | +ax.xaxis.set_major_formatter(mdates.DateFormatter('%b')) |
| 78 | +ax.set_xlabel('1980') |
| 79 | + |
| 80 | +# %% |
| 81 | +# |
| 82 | +# The default locator is the `~.dates.AutoDateLocator`, and the default |
| 83 | +# Formatter `~.dates.AutoDateFormatter`. There is also a "concise" |
| 84 | +# formatter/locator that gives a more compact labelling, and can be set via |
| 85 | +# rcParams. Note how instead of the redundant "Jan" label at the start of the |
| 86 | +# year, "1980" is used instead. See :ref:`date_concise_formatter` for more |
| 87 | +# examples. |
| 88 | + |
| 89 | +plt.rcParams['date.converter'] = 'concise' |
| 90 | + |
| 91 | +fig, ax = plt.subplots(figsize=(5.4, 2), layout='constrained') |
| 92 | +time = np.arange('1980-01-01', '1980-06-25', dtype='datetime64[D]') |
| 93 | +x = np.arange(len(time)) |
| 94 | +ax.plot(time, x) |
| 95 | + |
| 96 | +# %% |
| 97 | +# |
| 98 | +# We can set the limits on the axis either by passing the appropriate dates in |
| 99 | +# or by passing a floating point value in the proper units of floating days |
| 100 | +# since the epoch. We can get this value from `~.dates.date2num`. |
| 101 | + |
| 102 | +fig, axs = plt.subplots(2, 1, figsize=(5.4, 3), layout='constrained') |
| 103 | +for ax in axs.flat: |
| 104 | + time = np.arange('1980-01-01', '1980-06-25', dtype='datetime64[D]') |
| 105 | + x = np.arange(len(time)) |
| 106 | + ax.plot(time, x) |
| 107 | + |
| 108 | +# set xlim using datetime64: |
| 109 | +axs[0].set_xlim(np.datetime64('1980-02-01'), np.datetime64('1980-04-01')) |
| 110 | + |
| 111 | +# set xlim using floats: |
| 112 | +# Note can get from mdates.date2num(np.datetime64('1980-02-01')) |
| 113 | +axs[1].set_xlim(3683, 3683+60) |
| 114 | + |
| 115 | +# %% |
| 116 | +# |
| 117 | +# String conversion: categorical plots |
| 118 | +# ==================================== |
| 119 | +# |
| 120 | +# Sometimes we want to label categories on an axis rather than numbers. |
| 121 | +# Matplotlib allows this using a "categorical" converter (see |
| 122 | +# `~.matplotlib.category`). |
| 123 | + |
| 124 | +data = {'apple': 10, 'orange': 15, 'lemon': 5, 'lime': 20} |
| 125 | +names = list(data.keys()) |
| 126 | +values = list(data.values()) |
| 127 | + |
| 128 | +fig, axs = plt.subplots(1, 3, figsize=(7, 3), sharey=True, layout='constrained') |
| 129 | +axs[0].bar(names, values) |
| 130 | +axs[1].scatter(names, values) |
| 131 | +axs[2].plot(names, values) |
| 132 | +fig.suptitle('Categorical Plotting') |
| 133 | + |
| 134 | +# %% |
| 135 | +# |
| 136 | +# Note that the "categories" are plotted in the order that they are first |
| 137 | +# specified and that subsequent plotting in a different order will not affect |
| 138 | +# the original order. Further, new additions will be added on the end: |
| 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, and out of 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 <
F438
/td> |
| 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. However, note |
| 158 | +# that a float that is not a category will not get a label. |
| 159 | + |
| 160 | +fig, ax = plt.subplots(figsize=(5, 3), layout='constrained') |
| 161 | +ax.bar(names, values) |
| 162 | + |
| 163 | +# 0 gets labeled as "apple" |
| 164 | +ax.plot(0, 0, 'd', color='C1') |
| 165 | +ax.text(0, 0, ' Float x=0', rotation=45, color='C1') |
| 166 | + |
| 167 | +# 2 gets labeled as "lemon" |
| 168 | +ax.plot(2, 0, 'd', color='C1') |
| 169 | +ax.text(2, 0, ' Float x=2', rotation=45, color='C1') |
| 170 | + |
| 171 | +# 4 doesn't get a label |
| 172 | +ax.plot(4, 0, 'd', color='C1') |
| 173 | +ax.text(4, 0, ' Float x=4', rotation=45, color='C1') |
| 174 | + |
| 175 | +# 2.5 doesn't get a label |
| 176 | +ax.plot(2.5, 0, 'd', color='C1') |
| 177 | +ax.text(2.5, 0, ' Float x=2.5', rotation=45, color='C1') |
| 178 | + |
| 179 | + |
| 180 | +# %% |
| 181 | +# |
| 182 | +# Setting the limits for a category axis can be done by specifying the |
| 183 | +# categories, or by specifying floating point numbers: |
| 184 | + |
| 185 | +fig, axs = plt.subplots(2, 1, figsize=(5, 5), layout='constrained') |
| 186 | +ax = axs[0] |
| 187 | +ax.bar(names, values) |
| 188 | +ax.set_xlim('orange', 'lemon') |
| 189 | +ax.set_xlabel('limits set with categories') |
| 190 | +ax = axs[1] |
| 191 | +ax.bar(names, values) |
| 192 | +ax.set_xlim(0.5, 2.5) |
| 193 | +ax.set_xlabel('limits set with floats') |
| 194 | + |
| 195 | +# %% |
| 196 | +# |
| 197 | +# The category axes are helpful for some plot types, but can lead to confusion |
| 198 | +# if data is read in as a list of strings, even if it is meant to be a list of |
| 199 | +# floats or dates. This sometimes happens when reading comma-separated value |
| 200 | +# (CSV) files. The categorical locator and formatter will put a tick at every |
| 201 | +# string value and label each one as well: |
| 202 | + |
| 203 | +fig, ax = plt.subplots(figsize=(5.4, 2.5), layout='constrained') |
| 204 | +x = [str(xx) for xx in np.arange(100)] # list of strings |
| 205 | +ax.plot(x, np.arange(100)) |
| 206 | +ax.set_xlabel('x is list of strings') |
| 207 | + |
| 208 | +# %% |
| 209 | +# |
| 210 | +# If this is not desired, then simply convert the data to floats before plotting: |
| 211 | + |
| 212 | +fig, ax = plt.subplots(figsize=(5.4, 2.5), layout='constrained') |
| 213 | +x = np.asarray(x, dtype='float') # array of float. |
| 214 | +ax.plot(x, np.arange(100)) |
| 215 | +ax.set_xlabel('x is array of floats') |
| 216 | + |
| 217 | +# %% |
| 218 | +# |
| 219 | +# Determine converter, formatter, and locator on an axis |
| 220 | +# ====================================================== |
| 221 | +# |
| 222 | +# Sometimes it is helpful to be able to debug what Matplotlib is using to |
| 223 | +# convert the incoming data: we can do that by querying the ``converter`` |
| 224 | +# property on the axis. We can also query the formatters and locators using |
| 225 | +# `~.axis.Axis.get_major_locator` and `~.axis.Axis.get_major_formatter`. |
| 226 | +# |
| 227 | +# Note that by default the converter is *None*. |
| 228 | + |
| 229 | +fig, axs = plt.subplots(3, 1, figsize=(6.4, 7), layout='constrained') |
| 230 | +x = np.arange(100) |
| 231 | +ax = axs[0] |
| 232 | +ax.plot(x, x) |
| 233 | +label = f'Converter: {ax.xaxis.converter}\n ' |
| 234 | +label += f'Locator: {ax.xaxis.get_major_locator()}\n' |
| 235 | +label += f'Formatter: {ax.xaxis.get_major_formatter()}\n' |
| 236 | +ax.set_xlabel(label) |
| 237 | + |
| 238 | +ax = axs[1] |
| 239 | +time = np.arange('1980-01-01', '1980-06-25', dtype='datetime64[D]') |
| 240 | +x = np.arange(len(time)) |
| 241 | +ax.plot(time, x) |
| 242 | +label = f'Converter: {ax.xaxis.converter}\n ' |
| 243 | +label += f'Locator: {ax.xaxis.get_major_locator()}\n' |
| 244 | +label += f'Formatter: {ax.xaxis.get_major_formatter()}\n' |
| 245 | +ax.set_xlabel(label) |
| 246 | + |
| 247 | +ax = axs[2] |
| 248 | +data = {'apple': 10, 'orange': 15, 'lemon': 5, 'lime': 20} |
| 249 | +names = list(data.keys()) |
| 250 | +values = list(data.values()) |
| 251 | +ax.plot(names, values) |
| 252 | +label = f'Converter: {ax.xaxis.converter}\n ' |
| 253 | +label += f'Locator: {ax.xaxis.get_major_locator()}\n' |
| 254 | +label += f'Formatter: {ax.xaxis.get_major_formatter()}\n' |
| 255 | +ax.set_xlabel(label) |
| 256 | + |
| 257 | +# %% |
| 258 | +# |
| 259 | +# General unit support |
| 260 | +# ==================== |
| 261 | +# |
| 262 | +# The support for dates and categories is part of "units" support that is |
| 263 | +# built into Matplotlib. This is described at `.matplotlib.units` and in the # |
| 264 | +# :ref:`basic_units` example. |
| 265 | +# |
| 266 | +# Unit support works by querying the type of data passed to the plotting |
| 267 | +# function and dispatching to the first converter in a list that accepts that |
| 268 | +# type of data. So below, if ``x`` has ``datetime`` objects in it, the |
| 269 | +# converter will be ``_SwitchableDateConverter``; if it has has strings in it, |
| 270 | +# it will be sent to the ``StrCategoryConverter``. |
| 271 | + |
| 272 | +for k in munits.registry: |
| 273 | + print(f"type: {k};\n converter: {munits.registry[k]}") |
| 274 | + |
| 275 | +# %% |
| 276 | +# |
| 277 | +# Downstream libraries like pandas, astropy, and pint all can add their own |
| 278 | +# converters to Matplotlib. |
0 commit comments