Creating visualizations in Matplotlib goes beyond simply plotting data; it's about guiding your audience by emphasizing key points and providing context. Text and number annotations are essential for this purpose. Effective presentation involves more than adding text—thoughtful use of arrows and LaTeX for mathematical expressions can significantly enhance how your data is interpreted. This topic covers both the basics of text annotation and advanced techniques such as LaTeX integration and number formatting, helping you craft clear and impactful visualizations.
Understanding matplotlib.text.Text instances
In Matplotlib, the matplotlib.text.Text class is key to adding and customizing text elements within plots. Whether labeling axes, adding titles, or highlighting data points, understanding how to work with Text instances is crucial for clear and informative visualizations. Before exploring Text instances, let’s generate a style dictionary and visualize some data:
import numpy as np
import matplotlib as mpl
import matplotlib.pyplot as plt
from scipy.stats import norm, uniform
# Define a dictionary to customize the style of the plots
style_dict = {
"axes.linewidth": 1.5, "grid.color": "#DDDDDD", "font.family": "monospace",
"font.size": 12.0, "xtick.labelsize": 10, "ytick.labelsize": 10,
"xtick.major.size": 6, "ytick.major.size": 6, "xtick.minor.size": 3,
"ytick.minor.size": 3, "legend.fontsize": 9, "legend.framealpha": 0.5,
"figure.figsize": (6, 4), "savefig.dpi": 1000, "savefig.format": "svg"
}
# Monte Carlo estimate of the Integral of e^-(x^3)
estimates = []
stdvs = []
for i in range(1, 201):
unf = uniform.rvs(size = i * 1000)
expectation = np.exp(-unf ** 3)
estimates.append(np.mean(expectation))
stdvs.append(np.std(expectation) / np.sqrt(i * 1000))
mean_estimates = np.mean(estimates)
upper_bound = mean_estimates + 3 * np.array(stdvs)
lower_bound = mean_estimates - 3 * np.array(stdvs)plt.rcParams.update(style_dict)
# Add a horizontal line at the mean estimates value
plt.axhline(mean_estimates, xmin=0.05, xmax=0.95, alpha=0.5, c='r', zorder=201, ls='--')
# Plot the estimates as black dots with reduced opacity
plt.plot(estimates, 'k.', alpha=.3)
# Plot the upper and lower bounds as black dashed lines
plt.plot(upper_bound, 'k--')
plt.plot(lower_bound, 'k--')
# Fill the area between the lower and upper bounds with gray shading
plt.fill_between(range(200), lower_bound, upper_bound, color='gray', alpha=.2)
plt.show()Matplotlib functions that create text elements return a Text instance. These objects represent text elements on the plot and offer methods to customize properties such as font, size, color, and position. The first Text instance we'll explore is from the title and suptitle functions.
Use title to add a title for a single plot, summarizing its content. For figures with multiple subplots, use suptitle to provide an overarching summary. Let's add a title to the plot:
# Add a title to the plot and store the returned Text instance in the variable 'text_instance'
text_instance = plt.title('Monte Carlo Integration:\nIntegral of e^-(x^3)', loc='center')
plt.show()The loc parameter in plt.title() defaults to 'center' but can be also set to 'left' or 'right' to position the title accordingly. This flexibility enables you to position up to three titles—left, center, and right—and format each one differently. If you specify a new title for a position where a title already exists, it will overwrite the previous title for that specific location.
# Specifying more than one title
plt.title('Monte', loc='left')
plt.title('Carlo', loc='center', c='magenta')
plt.title('Integration', loc='right', fontname='Comic Sans MS', fontstyle='italic')To add both a title and a subtitle with specific formatting, use title() for the main title and text() for the subtitle. The plt.text() function allows you to place text at any location on the plot by specifying three positional arguments: x (the x-coordinate), y (the y-coordinate), and s (the text string).
# Set the main title of the plot and specify a pad
plt.title('Monte Carlo Integration:', loc='center', fontname='Comic Sans MS', fontstyle='italic', pad=26)
# Add a subtitle using normalized coordinates (plt.gca().transAxes) and horizontally align the text to the center (ha)
plt.text(.5, 1.05, s='Integral of e^-(x^3)', fontsize=10, fontfamily='monospace', ha='center', transform=plt.gca().transAxes)
plt.show()The x and y coordinates in plt.text() are relative to the axes of the plot, not the entire figure. By default, these coordinates are specified in data units, which means they correspond to positions within the plot's data space. However, if you use normalized coordinates with the transform parameter set to plt.gca().transAxes, the coordinates (0, 0) place the text at the bottom-left corner of the axes, and (1, 1) position it at the top-right corner of the axes. When using the title and text functions to add main and subtitle text, padding helps prevent visual clutter and ensures that the titles do not overlap with each other or the plot content and borders. The pad parameter controls the space between the title and the top of the plot, ensuring clear separation and readability.
More Text instances
Next, we’ll add axis labels and a legend, which are also Text instances. Since we are dealing with mathematical expressions, LaTeX is useful for accurate representation. Matplotlib supports LaTeX formatting for all text instances. The dollar sign ($) is used to denote the beginning and end of inline mathematical expressions. Let’s see how to apply it.
plt.rcParams.update(style_dict)
# Enter the label for the estimate line in LaTeX
plt.axhline(
mean_estimates, xmin=0.05, xmax=0.95, alpha=0.5, c='r',
zorder=201, ls='--', label=r"$\int_0^1e^{-x^3} = \frac{1}{3}\Gamma\frac{1}{3}$"
)
plt.plot(estimates, 'k.', alpha=.3)
plt.plot(upper_bound, 'k--', label='Bounds')
plt.plot(lower_bound, 'k--', label='_nolegend_')
plt.fill_between(range(200), lower_bound, upper_bound, color='gray', alpha=.2)
plt.title('Monte Carlo Integration:', loc='center', fontname='Comic Sans MS', fontstyle='italic', pad=26)
# Specify the subtitle in LaTeX
plt.text(.5, 1.05, s=r'Integral of $e^{-x^3}$', fontsize=10, fontfamily='monospace', ha='center', transform=plt.gca().transAxes)
# Specify the x and y labels
plt.ylabel('Estimates', labelpad=5)
plt.xlabel("Sample size ('000)", labelpad=5)
plt.legend()
plt.show()In Python, raw strings (r'') and escape character are crucial, especially for handling LaTeX in text. A raw string literal, prefixed with 'r', instructs Python to treat backslashes (\) as literal characters rather than escape sequences. This is especially useful in LaTeX, where backslashes denote commands (e.g., \frac{a}{b} for fractions). By using raw strings with LaTeX, you ensure that backslashes are interpreted correctly, avoiding issues with escape sequences in Python. You can learn more about LaTeX from the documentation. Now, let's customize the legend box by adding a shadow, giving it a title, organizing it into two columns, and positioning it below the x-axis:
plt.rcParams.update(style_dict)
plt.figure(figsize=(6, 6))
plt.axhline(mean_estimates, xmin=0.05, xmax=0.95, alpha=0.5, c='r', zorder=201, ls='--', label=r"$\int_0^1e^{-x^3} = \frac{1}{3}\Gamma\frac{1}{3}$")
plt.plot(estimates, 'k.', alpha=.3)
plt.plot(upper_bound, 'k--', label='Bounds')
plt.plot(lower_bound, 'k--', label='_nolegend_')
plt.fill_between(range(200), lower_bound, upper_bound, color='gray', alpha=.2)
plt.title('Monte Carlo Integration:', loc='center', fontname='Comic Sans MS', fontstyle='italic', pad=26)
plt.text(.5, 1.05, s=r'Integral of $e^{-x^3}$', fontsize=10, fontfamily='monospace', ha='center', transform=plt.gca().transAxes)
plt.ylabel('Estimates', labelpad=5)
plt.xlabel("Sample size ('000)", labelpad=5)
plt.legend(bbox_to_anchor=(.75, -.18), shadow=True, ncol=2, title='Legends')
plt.show()Rather than using LaTeX in the label parameter of the axhline function, you can apply it in the legend function. Additionally, you can use arrows to highlight key points in the graph. The plt.annotate function provides extensive options for this. We can add arrows to highlight the upper and lower bounds of the plot:
...
plt.annotate('Upper bound', xy=(25, .812), xytext=(50, .818), fontsize=10, fontfamily='monospace', arrowprops=dict(facecolor='white', edgecolor='black', width=3, headwidth=10, linewidth=1), ha='left', va='top')
plt.annotate('Lower bound', xy=(25, .803), xytext=(50, .800), fontsize=10, fontfamily='monospace', arrowprops=dict(facecolor='white', edgecolor='black', width=3, headwidth=10, linewidth=1), ha='left', va='top')
plt.show()The plt.annotate function allows you to control various aspects of annotations and arrows. The xy parameter specifies the coordinates of the point you want to annotate. The xytext parameter determines where the annotation text should appear relative to the xy point. You can use ha (horizontal alignment) and va (vertical alignment) to adjust the positioning of the text. The arrowprops parameter lets you customize the appearance of the arrow connecting the text to the xy point, while shrink adjusts the length of the arrow between the xy and xytext points.
Customizing Text instances
The setp function allows you to customize various aspects of Text instances, including labels, titles, and annotations. You can easily adjust properties like font size, color, rotation, and alignment for text elements without needing to dive into Matplotlib’s object-oriented structure. First, assign the Text instances to a variable. This variable is the positional parameter in the setp function, followed by additional parameters and their values for customization. The following code makes the previous plot using setp for the Text instances with additonal customization of the bounding box (bbox):
plt.rcParams.update(style_dict)
plt.figure(figsize=(6, 6))
# Customize the bounding box
bbox = dict(facecolor='.75', edgecolor='k', boxstyle='round')
# Using setp for the Text Instances
a = plt.axhline(mean_estimates, xmin=0.05, xmax=0.95)
plt.setp(a, alpha=0.5, c='r', zorder=201, ls='--', label=r"$\int_0^1e^{-x^3} = \frac{1}{3}\Gamma\frac{1}{3}$")
b = plt.plot(estimates,)
plt.setp(b, marker='.', linestyle='None', color='k', alpha=.3)
c = plt.plot(upper_bound)
plt.setp(c, c='k', ls='dashed', label='Bounds')
d = plt.plot(lower_bound)
plt.setp(d, c='k', ls='dashed', label='_nolegend_')
e = plt.fill_between(range(200), lower_bound, upper_bound)
plt.setp(e, color='gray', alpha=.2)
f = plt.title('Monte Carlo Integration:', loc='center', pad=40)
plt.setp(f, fontname='Comic Sans MS', fontstyle='italic', bbox=bbox)
g = plt.text(.5, 1.05, s=r'Integral of $e^{-x^3}$')
plt.setp(g, fontsize=10, fontfamily='monospace', ha='center', transform=plt.gca().transAxes, bbox=bbox)
h = plt.annotate('Upper bound', xy=(25, .812), xytext=(50, .818), arrowprops=dict(facecolor='white', edgecolor='black', width=3, headwidth=10, linewidth=1))
plt.setp(h, fontsize=10, fontfamily='monospace', ha='left', va='top')
i = plt.annotate('Lower bound', xy=(25, .803), xytext=(50, .800), arrowprops=dict(facecolor='white', edgecolor='black', width=3, headwidth=10, linewidth=1))
plt.setp(i, fontsize=10, fontfamily='monospace', ha='left', va='top')
plt.legend(bbox_to_anchor=(.75, -.18), shadow=True, title='Legends', ncol=2)
j = plt.ylabel('Estimates', labelpad=5)
plt.setp(j, bbox=bbox)
k = plt.xlabel("Sample size ('000)", labelpad=5)
plt.setp(k, bbox=bbox)
plt.show()The setp function allows you to set multiple properties at once and supports a wide range of text attributes, making it easy to achieve the desired appearance.
Customizing and formatting numbers
In data visualization, proper number formatting enhances the readability and clarity of your plots. Matplotlib offers various methods of number formatting. In this section, we'll use unemployment and average savings data:
unemployment = np.array(
[10.28853606, 9.59412948, 9.57687728, 8.78496393, 8.82590359,
8.40042225, 8.75780407, 7.99677564, 7.73401872, 7.05493108,
6.71271863, 6.43589482, 6.54505216, 5.83298602, 5.88401786,
6.47248301, 5.56599713, 4.66307519, 5.12234609, 4.01335362,
2.89398121, 3.1046525 , 2.36895187, 2.82424326]) / 100
savings = np.array(
[5009.20918959, 6490.39256918, 6467.81151297, 6846.97147473,
8023.39844277, 7155.30191071, 9030.85075732, 9950.64998825,
9478.09818146, 11441.44223899, 11690.98733418, 11966.26908653,
13142.47788957, 14613.60729847, 14221.36791014, 14906.7189888 ,
15205.10215893, 15662.03433701, 17154.29834305, 16963.26243487,
18079.26137948, 18456.82345053, 19587.31599983, 20166.83105264]) Let's use data from a specific index position and apply an f-string to display unemployment as a percentage, and perform currency formatting on savings at the specific index using the plt.annotate function:
idx = 15
xi, yi = unemployment[idx], savings[idx]
plt.annotate(f"({xi * 100:.2f}%, ${yi:,.0f})", xy=(xi, yi), textcoords="offset points", xytext=(-10, 10))
plt.scatter([xi], [yi], c='r', zorder=2)
plt.scatter(unemployment, savings)
plt.show()This can also be achieved with the format string function as follows:
plt.annotate("({:.2f}%, ${:,.0f})".format(xi * 100, yi), xy=(xi, yi), textcoords="offset points", xytext=(-10, 10))Numbers in axis labels and titles can be formatted using f-strings and format strings. However, formatting the major ticks on the axes requires a different approach. For tick labels, you can use the FuncFormatter class from matplotlib.ticker, which allows you to define a custom formatting function. The ticks on the graph can be formatted using the following method:
import matplotlib.ticker as ticker
plt.style.use("default")
plt.rcParams.update(style_dict)
idx = 15
xi, yi = unemployment[idx], savings[idx]
plt.annotate("({:.2f}%, ${:,.0f})".format(xi * 100, yi), xy=(xi, yi), textcoords="offset points", xytext=(-10, 10))
plt.scatter([xi], [yi], c='r', zorder=2)
def unemp_percent(value, pos):
# Formats the tick value as a percentage with one decimal place
return '{:0.1f}%'.format(value * 100)
def savings_currency(value, pos):
# Formats the tick value using leading zeros and a dollar sign
return r'$\${:0>9,.2f}$'.format(value)
plt.gca().yaxis.set_major_formatter(ticker.FuncFormatter(savings_currency))
plt.gca().xaxis.set_major_formatter(ticker.FuncFormatter(unemp_percent))
plt.scatter(unemployment, savings)
plt.show()Conclusion
Mastering text and number annotations in Matplotlib is key to creating clear and impactful visualizations. Whether adding titles, labels, or using LaTeX for mathematical expressions, these techniques allow you to guide your audience's focus and communicate data effectively. By customizing Text instances and using features like setp and annotate, you can transform your plots into compelling, informative visualizations. For more on annotation and formatting, refer to the Matplotlib documentation.