{
"cells": [
{
"cell_type": "markdown",
"metadata": {
"collapsed": true
},
"source": [
"# Advanced matplotlib\n",
"\n",
"Pierre Augier (LEGI), Cyrille Bonamy (LEGI), Eric Maldonado (Irstea), Franck Thollard (ISTerre), Christophe Picard (LJK), Loïc Huder (ISTerre)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Introduction\n",
"This is the second part of the introductive presentation given in the [Python initiation training](https://gricad-gitlab.univ-grenoble-alpes.fr/python-uga/py-training-2017/blob/master/ipynb/pres111_intro_matplotlib.ipynb). \n",
"\n",
"The aim is to present more advanced usecases of matplotlib."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Quick reminders"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"import numpy as np\n",
"import matplotlib.pyplot as plt"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"X = np.arange(0, 2, 0.01)\n",
"Y = np.exp(X) - 1\n",
"\n",
"plt.plot(X, X, linewidth=3)\n",
"plt.plot(X, Y)\n",
"plt.plot(X, X**2)\n",
"plt.xlabel('Abscisse')\n",
"plt.ylabel('Ordinate')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Object-oriented plots\n",
"\n",
"While doing the job, the previous example does not allow to unveil the power of matplotlib. For that, we need to keep in mind that in matplotlib plots, **everything** is an object.\n",
"\n",
"\n",
"It is therefore possible to change any aspect of the figure by acting on the appropriate objects. "
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### The same example with objects"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"fig = plt.figure()\n",
"print('Fig is an instance of', type(fig))\n",
"ax = fig.add_subplot(111) # More on subplots later...\n",
"print('Ax is an instance of', type(ax))\n",
"\n",
"X = np.arange(0, 2, 0.01)\n",
"Y = np.exp(X) - 1\n",
"\n",
"# Storing results of the plot\n",
"l1 = ax.plot(X, X, linewidth=3, label='Linear')\n",
"l2 = ax.plot(X, X**2, label='Square')\n",
"l3 = ax.plot(X, Y, label='$y = e^{x} - 1$')\n",
"\n",
"xlab = ax.set_xlabel('Abscissa')\n",
"ylab = ax.set_ylabel('Ordinate')\n",
"\n",
"ax.set_xlim(0, 2)\n",
"\n",
"ax.legend()"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# ax.plot returns in fact a list of the lines plotted by the instruction\n",
"print(type(l3))\n",
"# In this case, we plotted the lines one by one so l3 contains only the line corresponding to the exp function\n",
"exp_line = l3[0]\n",
"print(type(exp_line))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"This way, we can have access to the `Line2D` objects and therefore to all their attributes (and change them!). This includes:\n",
"- **get_data/set_data**: to get/set the numerical xdata, ydata of the line\n",
"- **get_color/set_color**: to get/set the color of the line\n",
"- **get_marker/set_marker**: to get/set the markers\n",
"- ...\n",
"\n",
"See https://matplotlib.org/api/_as_gen/matplotlib.lines.Line2D.html for the complete list.\n",
"\n",
"_Note: `Line2D` is based on the `Artist` class from which any graphical element inherits (lines, ticks, axes...)._"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### An application: ticks\n",
"\n",
"A common manipulation when designing figures for articles is to change the ticks location. Matplotlib provides several ways to do this\n",
"\n",
"#### Direct manipulation"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"from calendar import day_name\n",
"\n",
"weekdays = list(day_name)\n",
"print(weekdays)\n",
"temperatures = [20., 22., 16., 18., 17., 19., 20.]\n",
"\n",
"fig, ax = plt.subplots()\n",
"\n",
"ax.plot(temperatures, marker='o', markersize=10)\n",
"ax.set_xlabel('Weekday')\n",
"ax.set_ylabel('Temperature ($^{\\circ}C$)')"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# Change locations\n",
"ax.set_yticks(np.arange(15, 25, 0.5))\n",
"\n",
"# Change locations AND labels\n",
"ax.set_xticks(range(7))\n",
"ax.set_xticklabels(weekdays)\n",
"\n",
"ax.set_xlabel('')\n",
"\n",
"# Show the updated figure\n",
"fig"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"#### With `Locators`\n",
"`Locators` are objects that give rules to generate the tick locations. See https://matplotlib.org/api/ticker_api.html.\n",
"
"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"For example, for the yticks in the previous example, we could have done"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"import matplotlib.ticker as ticker\n",
"\n",
"# Change locator for the major ticks of yaxis\n",
"ax.yaxis.set_major_locator(ticker.MultipleLocator(0.5))\n",
"\n",
"fig"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"#### Major and minor ticks\n",
"matplotlib provides two types of ticks: major and minor. The parameters and aspect of the two kinds can be handled separately."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"import matplotlib.ticker as ticker\n",
"\n",
"# Change locator for the major ticks of yaxis\n",
"ax.yaxis.set_major_locator(ticker.MultipleLocator(1.))\n",
"ax.yaxis.set_minor_locator(ticker.MultipleLocator(0.5))\n",
"\n",
"fig"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### An application: subplots"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"fig, axes = plt.subplots(nrows=1, ncols=3, sharey=True)\n",
"\"\"\"\n",
"# Equivalent to\n",
"fig = plt.figure()\n",
"axes = []\n",
"axes.append(fig.add_subplot(131))\n",
"axes.append(fig.add_subplot(132, sharey=axes[0]))\n",
"axes.append(fig.add_subplot(133, sharey=axes[0]))\n",
"\"\"\"\n",
"\n",
"# This is only to have the same colors as before\n",
"color_cycle = plt.rcParams['axes.prop_cycle'].by_key()['color']\n",
"\n",
"X = np.arange(0, 2, 0.01)\n",
"Y = np.exp(X) - 1\n",
"\n",
"axes[0].set_title('Linear')\n",
"axes[0].plot(X, X, linewidth=3, color=color_cycle[0])\n",
"axes[1].set_title('Square')\n",
"axes[1].plot(X, X**2, label='Square', color=color_cycle[1])\n",
"axes[2].set_title('$y = e^{x} - 1$')\n",
"axes[2].plot(X, Y, label='$y = e^{x} - 1$', color=color_cycle[2])\n",
"\n",
"axes[0].set_ylabel('Ordinate')\n",
"for ax in axes:\n",
" ax.set_xlabel('Abscissa')\n",
" ax.set_xlim(0, 2)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Fancier subplots with gridspec"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"import matplotlib.gridspec as gridspec\n",
"\n",
"fig = plt.figure()\n",
"gs = gridspec.GridSpec(2, 2, figure=fig) # 2 rows and 2 columns\n",
"X = np.arange(-3, 3, 0.01)*np.pi\n",
"ax1 = fig.add_subplot(gs[0,0]) # 1st row, 1st column\n",
"ax2 = fig.add_subplot(gs[1,0]) # 2nd row, 1st column\n",
"ax3 = fig.add_subplot(gs[:,1]) # all rows, 2nd column\n",
"ax1.plot(X, np.cos(2*X), color=\"red\")\n",
"ax2.plot(X, np.sin(2*X), color=\"magenta\")\n",
"ax3.plot(X, X**2)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Interactivity (https://matplotlib.org/users/event_handling.html)\n",
"\n",
"Know first that other plotting libraries offers interactions more smoothly (`plotly`, `bokeh`, ...). Nevertheless, `matplotlib` gives access to backend-independent methods to add interactivity to plots.\n",
"\n",
"These methods use [`Events`](https://matplotlib.org/api/backend_bases_api.html#matplotlib.backend_bases.Event) to catch user interactions (mouse clicks, key presses, mouse hovers, etc...).\n",
"\n",
"These events must be connected to callback functions using the `mpl_connect` method of `Figure.Canvas`:\n",
"\n",
"```python\n",
"fig = plt.figure()\n",
"fig.canvas.mpl_connect(self, event_name, callback_func)\n",
"```\n",
"The signature of `callback_func` is:\n",
"```python\n",
" def callback_func(event)\n",
"```\n",
"where event is a `matplotlib.backend_bases.Event`. The following events are recognized\n",
"\n",
"- **'button_press_event'**\n",
"- 'button_release_event'\n",
"- 'draw_event'\n",
"- **'key_press_event'**\n",
"- 'key_release_event'\n",
"- 'motion_notify_event'\n",
"- 'pick_event'\n",
"- 'resize_event'\n",
"- 'scroll_event'\n",
"- 'figure_enter_event',\n",
"- 'figure_leave_event',\n",
"- 'axes_enter_event',\n",
"- 'axes_leave_event'\n",
"- **'close_event'**\n",
"\n",
"N.B. : `Figure.Canvas` takes care of the rendering of the figure (independent of the used backend) which is why it is used to handle events."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### A simple example: changing the color of a line"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# Jupyter command to enable interactivity\n",
"%matplotlib notebook\n",
"\n",
"f = plt.figure()\n",
"ax = f.add_subplot(111)\n",
"\n",
"X = np.arange(0, 10, 0.01)\n",
"l, = plt.plot(X, X**2)\n",
"\n",
"def change_color(event):\n",
" l.set_color('green')\n",
" \n",
"f.canvas.mpl_connect('button_press_event', change_color)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Enhanced interactivity: a drawing tool"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"f2 = plt.figure()\n",
"ax2 = f2.add_subplot(111)\n",
"ax2.set_aspect('equal')\n",
"x_data = []\n",
"y_data = []\n",
"l, = ax2.plot(x_data, y_data, marker='o')\n",
"\n",
"def add_datapoint(event):\n",
" x_data.append(event.xdata)\n",
" y_data.append(event.ydata)\n",
" l.set_data(x_data, y_data)\n",
"\n",
"f2.canvas.mpl_connect('button_press_event', add_datapoint)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"But, here we are referencing `x_data` and `y_data` in `add_datapoint` that are defined outside the function : this breaks encapsulation ! \n",
"\n",
"A nicer solution would be to use an object to handle the interactivity. We can also take advantage of this to add more functionality (such as clearing of the figure when the mouse exits) :"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"class InteractivePlot():\n",
" def __init__(self, figure):\n",
" self.ax = figure.add_subplot(111)\n",
" self.ax.set_aspect('equal')\n",
" self.x_data = []\n",
" self.y_data = []\n",
" self.interactive_line, = self.ax.plot(self.x_data, self.y_data, marker='o')\n",
" # Need to keep the callbacks references in memory to have the interactivity\n",
" self.button_callback = figure.canvas.mpl_connect('button_press_event', self.add_datapoint)\n",
" self.clear_callback = figure.canvas.mpl_connect('figure_leave_event', self.clear)\n",
" \n",
" def add_datapoint(self, event):\n",
" if event.button == 1: # Left click\n",
" self.x_data.append(event.xdata)\n",
" self.y_data.append(event.ydata)\n",
" self.update_line()\n",
" elif event.button == 3: # Right click\n",
" self.x_data = []\n",
" self.y_data = []\n",
" self.interactive_line, = self.ax.plot(self.x_data, self.y_data, marker='o')\n",
" \n",
" def clear(self, event):\n",
" self.ax.clear()\n",
" self.x_data = []\n",
" self.y_data = []\n",
" self.interactive_line, = self.ax.plot(self.x_data, self.y_data, marker='o')\n",
" \n",
" def update_line(self):\n",
" self.interactive_line.set_data(self.x_data, self.y_data)\n",
" \n",
"\n",
"f = plt.figure()\n",
"ip = InteractivePlot(f)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"More examples could be shown but it always revolves around the same process: connecting an `Event` to a callback function.\n",
"\n",
"Note that the connection can be severed using `mpl_disconnect` that takes the callback id in arg (in the previous case `self.button_callback` or `self.clear_callback`.\n",
"\n",
"Some usages of interactivity:\n",
"- Print the value of a point on click\n",
"- Trigger a plot in the third dimension of a 3D plot displayed in 2D\n",
"- Save a figure on closing\n",
"- Ideas ?"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Animations\n",
"\n",
"From the matplotlib page (https://matplotlib.org/api/animation_api.html):\n",
"> The easiest way to make a live animation in matplotlib is to use one of the Animation classes.\n",
">
FuncAnimation | Makes an animation by repeatedly calling a function func. |
ArtistAnimation | Animation using a fixed set of Artist objects. |