Plotting dates
Sooner or later, we all have had the need to plot some information over time, be it for the bank account balance each month, the total web site accesses for each day of the year, or one of many other reasons.
Matplotlib has a plotting function ad hoc for dates, plot_date() that considers data on X, Y, or both axes, as dates, labeling the axis accordingly.
As usual, we now present an example, and we will discuss it later:
In [1]: import matplotlib as mpl
In [2]: import matplotlib.pyplot as plt
In [3]: import numpy as np
In [4]: import datetime as dt
In [5]: dates = [dt.datetime.today() + dt.timedelta(days=i)
...: for i in range(10)]
In [6]: values = np.random.rand(len(dates))
In [7]: plt.plot_date(mpl.dates.date2num(dates), values, linestyle='-
');
In [8]: plt.show()
First, a note about linestyle keyword argument: without it, there’s no line connecting the markers that are displayed alone.
We created the dates array using timedelta(), a datetime function that helps us define a date interval—10 days in this case. Note how we had to convert our date values using the date2num() function. This is because Matplotlib represents dates as float values corresponding to the number of days since 0001-01-01 UTC.
Also note how the X-axis labels, the ones that have data values, are badly rendered.
Matplotlib provides ways to address the previous two points—date formatting and conversion, and axes formatting.
Date formatting
Commonly, in Python programs, dates are represented as datetime objects, so we have to first convert other data values into datetime objects, sometimes by using the dateutil companion module, for example:
import datetime
date = datetime.datetime(2009, 03, 28, 11, 34, 59, 12345)
or
import dateutil.parser
datestrings = ['2008-07-18 14:36:53.494013','2008-07-20
14:37:01.508990', '2008-07-28 14:49:26.183256']
dates = [dateutil.parser.parse(s) for s in datestrings]
Once we have the datetime objects, in order to let Matplotlib use them, we have to convert them into floating point numbers that represent the number of days since 0001-01-01 00:00:00 UTC.
To do that, Matplotlib itself provides several helper functions contained in the matplotlib.dates module:
- date2num(): This function converts one or a sequence of datetime objects to float values representing days since 0001-01-01 00:00:00 UTC (the fractional parts represent hours, minutes, and seconds)
- num2date(): This function converts one or a sequence of float values representing days since 0001-01-01 00:00:00 UTC to datetime objects (or a sequence, if the input is a sequence)
- drange(dstart, dend, delta): This function returns a date range (a sequence) of float values in Matplotlib date format; dstart and dend are datetime objects while delta is a datetime.timedelta instance
Usually, what we will end up doing is converting a sequence of datetime objects into a Matplotlib representation, such as:
dates = list of datetime objects
mpl_dates = matplotlib.dates.date2num(dates)
drange() can be useful in situations like this one:
import matplotlib as mpl
from matplotlib import dates
import datetime as dt
date1 = dt.datetime(2008, 9, 23)
date2 = dt.datetime(2009, 4, 12)
delta = dt.timedelta(days=10)
dates = mpl.dates.drange(date1, date2, delta)
where dates will be a sequence of floats starting from date1 and ending at date2 with a delta timestamp between each item of the list.
Axes formatting with axes tick locators and formatters
As we have already seen, the X labels on the first image are not that nice looking. We would expect Matplotlib to allow a better way to label the axis, and indeed, there is.
The solution is to change the two parts that form the axis ticks—locators and formatters. Locators control the tick’s position, while formatters control the formatting of labels. Both have a major and minor mode: the major locator and formatter are active by default and are the ones we commonly see, while minor mode can be turned on by passing a relative locator or formatter function (because minors are turned off by default by assigning NullLocator and NullFormatter to them).
While this is a general tuning operation and can be applied to all Matplotlib plots, there are some specific locators and formatters for date plotting, provided by matplotlib.dates:
- MinuteLocator, HourLocator,DayLocator, WeekdayLocator,MonthLocator, YearLocator are all the locators available that place a tick at the time specified by the name, for example, DayLocator will draw a tick at each day. Of course, a minimum knowledge of the date interval that we are about to draw is needed to select the best locator.
- DateFormatter is the tick formatter that uses strftime() to format strings.
The default locator and formatter are matplotlib.ticker.AutoDateLocator and matplotlib.ticker.AutoDateFormatter, respectively. Both are set by the plot_date() function when called. So, if you wish to set a different locator and/or formatter, then we suggest to do that after the plot_date() call in order to avoid the plot_date() function resetting them to the default values.
Let’s group all this up in an example:
In [1]: import matplotlib as mpl
In [2]: import matplotlib.pyplot as plt
In [3]: import numpy as np
In [4]: import datetime as dt
In [5]: fig = plt.figure()
In [6]: ax2 = fig.add_subplot(212)
In [7]: date2_1 = dt.datetime(2008, 9, 23)
In [8]: date2_2 = dt.datetime(2008, 10, 3)
In [9]: delta2 = dt.timedelta(days=1)
In [10]: dates2 = mpl.dates.drange(date2_1, date2_2, delta2)
In [11]: y2 = np.random.rand(len(dates2))
In [12]: ax2.plot_date(dates2, y2, linestyle='-');
In [13]: dateFmt = mpl.dates.DateFormatter('%Y-%m-%d')
In [14]: ax2.xaxis.set_major_formatter(dateFmt)
In [15]: daysLoc = mpl.dates.DayLocator()
In [16]: hoursLoc = mpl.dates.HourLocator(interval=6)
In [17]: ax2.xaxis.set_major_locator(daysLoc)
In [18]: ax2.xaxis.set_minor_locator(hoursLoc)
In [19]: fig.autofmt_xdate(bottom=0.18) # adjust for date labels
display
In [20]: fig.subplots_adjust(left=0.18)
In [21]: ax1 = fig.add_subplot(211)
In [22]: date1_1 = dt.datetime(2008, 9, 23)
In [23]: date1_2 = dt.datetime(2009, 2, 16)
In [24]: delta1 = dt.timedelta(days=10)
In [25]: dates1 = mpl.dates.drange(date1_1, date1_2, delta1)
In [26]: y1 = np.random.rand(len(dates1))
In [27]: ax1.plot_date(dates1, y1, linestyle='-');
In [28]: monthsLoc = mpl.dates.MonthLocator()
In [29]: weeksLoc = mpl.dates.WeekdayLocator()
In [30]: ax1.xaxis.set_major_locator(monthsLoc)
In [31]: ax1.xaxis.set_minor_locator(weeksLoc)
In [32]: monthsFmt = mpl.dates.DateFormatter('%b')
In [33]: ax1.xaxis.set_major_formatter(monthsFmt)
In [34]: plt.show()
The result of executing the previous code snippet is as shown:
We drew the subplots in reverse order to avoid some minor overlapping problems.
fig.autofmt_xdate() is used to nicely format date tick labels. In particular, this function rotates the labels (by using rotation keyword argument, with a default value of 30°) and gives them more room (by using bottom keyword argument, with a default value of 0.2).
We can achieve the same result, at least for the additional spacing, with:
fig = plt.figure()
fig.subplots_adjust(bottom=0.2)
ax = fig.add_subplot(111)
This can also be done by creating the Axes instance directly with:
ax = fig.add_axes([left, bottom, width, height])
while specifying the explicit dimensions.
The subplots_adjust() function allows us to control the spacing around the subplots by using the following keyword arguments:
- bottom, top, left, right: Controls the spacing at the bottom, top, left, and right of the subplot(s)
- wspace, hspace: Controls the horizontal and vertical spacing between subplots
We can also control the spacing by using these parameters in the Matplotlib configuration file:
figure.subplot.<position> = <value>
Custom formatters and locators
Even if it’s not strictly related to date plotting, tick formatters allow for custom formatters too:
...
import matplotlib.ticker as ticker
...
def format_func(x, pos):
return <a transformation on x>
...
formatter = ticker.FuncFormatter(format_func)
ax.xaxis.set_major_formatter(formatter)
...
The function format_func will be called for each label to draw, passing its value and position on the axis. With those two arguments, we can apply a transformation (for example, divide x by 10) and then return a value that will be used to actually draw the tick label.
Here’s a general note on NullLocator: it can be used to remove axis ticks by simply issuing:
ax.xaxis.set_major_locator(matplotlib.ticker.NullLocator())
Text properties, fonts, and LaTeX
Matplotlib has excellent text support, including mathematical expressions, TrueType font support for raster and vector outputs, newline separated text with arbitrary rotations, and Unicode.
We have total control over every text property (font size, font weight, text location, color, and so on) with sensible defaults set in the rc configuration file. Specifically for those interested in mathematical or scientific figures, Matplotlib implements a large number of TeX math symbols and commands to support mathematical expressions anywhere in the figure.
We already saw some text functions, but the following list contains all the functions which can be used to insert text with the pyplot interface, presented along with the corresponding API method and a description:
Pyplot function |
API method |
Description |
text() |
mpl.axes.Axes.text() |
Adds text at an arbitrary location to the Axes |
xlabel() |
mpl.axes.Axes.set_xlabel() |
Adds an axis label to the X-axis |
ylabel() |
mpl.axes.Axes.set_ylabel() |
Adds an axis label to the Y-axis |
title() |
mpl.axes.Axes.set_title() |
Adds a title to the Axes |
figtext() |
mpl.figure.Figure.text() |
Adds text at an arbitrary location to the Figure |
suptitle() |
mpl.figure.Figure.suptitle() |
Adds a centered title to the Figure |
annotate() |
mpl.axes.Axes.annotate() |
Adds an annotation with an optional arrow to the Axes |
All of these commands return a matplotlib.text.Text instance. We can customize the text properties by passing keyword arguments to the functions or by using matplotlib.artist.setp():
t = plt.xlabel('some text', fontsize=16, color='green')
We can do it as:
t = plt.xlabel('some text')
plt.setp(t, fontsize=16, color='green')
Handling objects allows for several new possibilities; such as setting the same property to all the objects in a specific group. Matplotlib has several convenience functions to return the objects of a plot. Let’s take the example of the tick labels:
ax.get_xticklabels()
This line of code returns a sequence of object instances (the labels for the X-axis ticks) that we can tune:
for t in ax.get_xticklabels():
t.set_fontsize(5.)
or else, still using setp():
setp(ax.get_xticklabels(), fontsize=5.)
It can take a sequence of objects, and apply the same property to all of them.
To recap, all of the properties such as color, fontsize, position, rotation, and so on, can be set either:
- At function call using keyword arguments
- Using setp() referencing the Text instance
- Using the modification function
Fonts
Where there is text, there are also fonts to draw it. Matplotlib allows for several font customizations.
The most complete documentation on this is currently available in the Matplotlib configuration file, /etc/matplotlibrc. We are now reporting that information here.
There are six font properties available for modification.
Property name |
Values and description |
font.family |
It has five values:
Each of these font families has a default list of font names in decreasing order of priority associated with them (next table). In addition to these generic font names, font.family may also be an explicit name of a font available on the system. |
font.style |
Three values: normal (or roman), italic, or oblique. The oblique style will be used for italic, if it is not present. |
font.variant |
Two values: normal or small-caps. For TrueType fonts, which are scalable fonts, small-caps is equivalent to using a font size of smaller, or about 83% of the current font size. |
font.weight |
Effectively has 13 values-normal, bold, bolder, lighter, 100, 200, 300, …, 900. normal is the same as 400, and bold is 700. bolder and lighter are relative values with respect to the current weight. |
font.stretch |
11 values-ultra-condensed, extra-condensed, condensed, semi-condensed, normal, semi-expanded, expanded, extra-expanded, ultra-expanded, wider, and narrower. This property is not currently implemented. It works if the font supports it, but only few do. |
font.size |
The default font size for text, given in points. 12pt is the standard value. |
Where’sis that place on the photo?