{ "cells": [ { "cell_type": "markdown", "id": "e48ed5ab-b8dd-4868-b5aa-28d6d7741e9c", "metadata": {}, "source": [ "# Plotting in Python with Matplotlib\n", "## Winter visualization series\n", "
\n", "J. Yohai Meiron, PhD
\n", "SciNet High Performance Computing Consortium
\n", "University of Toronto\n", "
\n", "\n", "
\n", "\n", "\n", "
\n", "\n", "

The University of Toronto is located on the traditional land of
the Huron-Wendat, the Seneca, and the Mississaugas of the Credit.

\n" ] }, { "cell_type": "markdown", "id": "198a1e0e-01d3-423f-9993-f6eff61ed639", "metadata": {}, "source": [ "#### Abstract\n", "Matplotlib is the foundational plotting library for Python, and is widely used in tandem with other scientific libraries (such as NumPy and Pandas) to visualize data across many different fields. It is a free and open source software library that offers fine-grained control over every aspect of a plot, making it a powerful tool for customizing figures to meet specific needs. In this workshop, we will use a Jupyter notebook to show you how to create common 2D plot types such as line, scatter, and heatmaps, and how to customize the labels, legend, and panel layout. We will briefly touch on using Matplotlib to produce animations and interactive visualizations. By the end of this one-hours session, you will have a basic understanding of the library’s capabilities.\n", "\n", "#### How to follow?\n", "Notebook link: https://pages.scinet.utoronto.ca/~ymeiron/matplotlib-demo.ipynb\n", "* I'm using SciNet [Open OnDemand](https://ondemand.scinet.utoronto.ca) JupyterLab. Use that if you have access to the Trillium system.\n", "* If you don't, you can install the `notebook`, `matplotlib`, `pandas` and `ipympl` Python packages and the `ffmpeg` software *locally*, and launch a notebook server.\n", "* You can use a free (or paid) cloud Jupyter notebook environment (Google Colab, Binder, Kaggle). Note that not all support interactive plots.\n", "* We have a Magic Castle instance for this lecture (details provided separately). This *does not* support interactive plots however.\n", "\n", "Unfortunately we won't have time to assist with technical difficulties today." ] }, { "cell_type": "markdown", "id": "eb1057b4-93ee-499c-b4ae-f79f769be936", "metadata": {}, "source": [ "## Introduction\n", "\n", "Matplotlib (MPL) is a plotting library for Python, launched in 2003 and inspired by MATLAB. Today we are going to learn the basics of working with it, and I'm going to demo a few common types of plots, but what we can show in an hour is really the tip of the iceberg in terms of what's possible. The best way to learn further is to go to the [gallery](https://matplotlib.org/stable/gallery/index.html) and find a figure with features you are interested in, and look at the source code to learn how it was made.\n", "\n", "MPL provides a high level API (object-oriented as well as state-based) for creating figures, using several possible backends. This API is called PyPlot, let's import it and draw our first figure." ] }, { "cell_type": "code", "execution_count": null, "id": "f8e1befd-157f-4f13-990e-87b5db0e538d", "metadata": {}, "outputs": [], "source": [ "from matplotlib import pyplot as plt" ] }, { "cell_type": "code", "execution_count": null, "id": "f4895b4c-d03f-47c0-924b-d3e4f2826fde", "metadata": {}, "outputs": [], "source": [ "fig, ax = plt.subplots()" ] }, { "cell_type": "markdown", "id": "595e7695-d99b-4c83-9954-da96ee0d4c98", "metadata": {}, "source": [ "The `subplots` function, called with no parameters here, caused this image to be added to our notebook and returned `Figure` and `Axes` objects. The figure is the top level container for all the plot elements. Each visible element, or an object that can draw itself on a figure, is called an *artist* in MPL terminology (the figure itself is also technically an artist). The axes object (or *subfigure*) is an artist contained in the figure, and it itself contains other artists (such as the ticks and labels, and plot elements once we add them). Artists have member functions that we can use to manipulate them, as we shall see. Axes are optional but in this lecture we'll always use them, and we can have more than one set of axes in the figure.\n", "\n", "πŸ›ˆ This is not the only way to create a figure with axes. In fact, there are usually multiple ways to do anything in Matplotlib, in violation of [the Zen of Python](https://peps.python.org/pep-0020/#the-zen-of-python). Here we'll stick to an object-oriented coding style and avoid shortcuts. This will produce a code that is slightly longer, but more readable.\n", "\n", "### Backends\n", "\n", "Let's talk about the figure appearing in the notebook. While this was just one line, obviously there is a lot of low level *backend* code that's involved here. MPL can use several different backends to actually create an image (be it static or interactive). We don't need to know much about what they do and how, but just that we have the choice.\n", "\n", "#### Jupyter notebooks\n", "\n", "The default backend in Jupyter notebooks is called `inline`. It essentially creates a PNG image (a static bitmap) from the figure, that is embedded into the notebook web page. Here, we cannot interact with the plot.\n", "\n", "An alternative backend for Jupyter Notebooks is `ipympl` (requires a package of the same name to be installed). It renders the figure as an applet using web technologies (such as JavaScript) that get embedded in the notebook web page. Here, we can interact with the figure using the mouse as we are going to see. To switch to this backend use the `%matplotlib ipympl` Jupyter \"magic\" command.\n", "\n", "πŸ›ˆ In a notebook, if we create a figure in one cell, the `Figure` object still exists in the following cells (until it's discarded, e.g. the `fig` name reassigned). If it (or subordinate artists) is manipulated in the following cells, this will only affect what we see embedded in the notebook if we are using the `ipympl` backend. If the `inline` backend is used, commands run in the following cells won't affect the already-displayed figure. In this case you can draw the figure again using `display(fig)`." ] }, { "cell_type": "code", "execution_count": null, "id": "99c383de-b3b1-4f28-9f5c-a026894937d7", "metadata": {}, "outputs": [], "source": [ "%matplotlib ipympl\n", "# Run this cell and go back to the cell where we created the previous figure, and run it again." ] }, { "cell_type": "markdown", "id": "e9a861c7-f519-4d29-883f-dcd72c4bef2d", "metadata": {}, "source": [ "#### Interactive backends and the `show` function\n", "\n", "If the Python code runs as a standalone program through the command line, you'll usually use a backend that renders the figure in a graphical window. The backend will normally be chosen automatically based on the operating system and Python/MPL installation, but can be changed using the `matplotlib.use` function.\n", "\n", "**Importantly**, when all figures are ready, use `plt.show()` to indicate to the backend to open the graphical windows (one per figure).\n", "\n", "#### Static backends and the `savefig` function\n", "\n", "If the Python code runs in a non-interactive environment, such as when running a batch job in an HPC cluster, you'll use a *static* backend, that will not attempt to render anything to the screen. In most cases the library will know that there is no display available, but if MPL is trying to use an interactive backend for some reason and is producing an error, you can change it by using the `matplotlib.use` function (and choosing a static backend such as `agg`).\n", "\n", "In those cases, you will save a figure to a file. The static backend will be chosen automatically based on the file extension. For example:" ] }, { "cell_type": "code", "execution_count": null, "id": "1eefd783-61d1-4e99-8a53-b0d93df70ece", "metadata": {}, "outputs": [], "source": [ "fig.savefig('simple_figure.pdf')" ] }, { "cell_type": "markdown", "id": "3ebeb923-b17a-4ff3-8ac9-60dae991d706", "metadata": {}, "source": [ "## Line plot\n", "\n", "Let's create our first *plot*." ] }, { "cell_type": "code", "execution_count": null, "id": "a83ff3c6-ad0c-4a04-8ec2-54a9303e5b6c", "metadata": {}, "outputs": [], "source": [ "import numpy as np\n", "x = np.linspace(0, 16, 256)\n", "y1 = np.sin(x)\n", "y2 = np.cos(x)\n", "\n", "fig, ax = plt.subplots()\n", "ax.plot(x, y1, label='sine')\n", "ax.plot(x, y2, label='cosine')\n", "ax.legend()\n", "ax.set_xlabel('x')\n", "ax.set_ylabel('y')\n", "ax.set_title('Trigonometric functions');\n", "\n", "# The semicolon at the end of the last command just suppresses some unwanted output in the Jupyter notebook" ] }, { "cell_type": "markdown", "id": "a87550ff-41e4-41b9-94eb-76afe0e484a8", "metadata": {}, "source": [ "πŸ›ˆ The alternative to this object-oriented approach is to use the `plt.plot` function, which creates the figure and axes if they don't already exist.\n", "\n", "## The global style\n", "\n", "As we just saw, sensible defaults are used when creating a new figure. We didn't have to specify what font to use, what colour to use for each line, where to place the ticks, etc. In this case we just used MPL's defaults, giving us this signature look.\n", "\n", "### Runtime configuration settings\n", "\n", "When producing many figures (e.g. for publication, or a poster), we might want to tweak these defaults, so our figures have a consistent look. To do this we can tweak the runtime configuration settings by manipulating `matplotlib.rcParams`. For example:" ] }, { "cell_type": "code", "execution_count": null, "id": "9911c27c-3de5-4075-824f-bad93f784c59", "metadata": {}, "outputs": [], "source": [ "import matplotlib as mpl\n", "mpl.rcParams['font.family'] = 'Futura LT'\n", "# Run this cell and go back to the cell where we created the previous figure, and run it again." ] }, { "cell_type": "markdown", "id": "07e31b4d-4b7a-4f77-93c2-9d36afe4c2ba", "metadata": {}, "source": [ "Will change the default font of artists (reminder: this means figure elements) to Futura, in all future figures in this notebook.\n", "\n", "There are (currently) 322 different settings (not all style-related)! So MPL plots are extremely customizable. You can find them listed [here](https://matplotlib.org/stable/api/matplotlib_configuration_api.html#matplotlib.rcParams).\n", "\n", "⚠️ When choosing a font, it has to be installed on the machine where the Python interpreter (for the Jupyter notebook in this case) is running.\n", "\n", "### Style sheets\n", "In addition to MPL's \"signature\" look, additional pre-defined styles are available (see them listed [here](https://matplotlib.org/stable/gallery/style_sheets/style_sheets_reference.html)). Load them with `plt.style.use` (and of course, you can still tweak the chosen style by manipulating `matplotlib.rcParams`)" ] }, { "cell_type": "code", "execution_count": null, "id": "065fc953-9941-4bcd-9b00-007dd55123af", "metadata": {}, "outputs": [], "source": [ "plt.style.use('classic') # Also try: ggplot, fivethirtyeight, bmh\n", "# Run this cell and go back to the cell where we created the previous figure, and run it again." ] }, { "cell_type": "code", "execution_count": null, "id": "5b142150-b24f-4f3e-a893-97f363f24d4f", "metadata": {}, "outputs": [], "source": [ "# Restore defaults values of all runtime configuration settings\n", "plt.rcdefaults()" ] }, { "cell_type": "markdown", "id": "36e7ceae-980e-435d-be73-bc851a1ccd00", "metadata": {}, "source": [ "## Keyword arguments\n", "\n", "The most common way to customize part of the figure is to pass keyword arguments to the function that creates the artist. In the line plot example, the first line was blue and the second orange, this colour sequence is defined in the default style sheet. We can manipulate the line's characteristics by passing suitable keywords that the `plot` function can accept (see [documentations](https://matplotlib.org/stable/api/_as_gen/matplotlib.axes.Axes.plot.html))." ] }, { "cell_type": "code", "execution_count": null, "id": "4281f373-6cb8-4309-a4e2-d76dfb04efd0", "metadata": {}, "outputs": [], "source": [ "fig, ax = plt.subplots()\n", "l, = ax.plot(x, y1, color='r', linewidth=4, linestyle='--')" ] }, { "cell_type": "markdown", "id": "c0237c29-4e0c-47c8-884b-dfd6f9ac7a89", "metadata": {}, "source": [ "* The string `r` is a single character shorthand notation for \"red\", there are [many other ways](https://matplotlib.org/stable/users/explain/colors/colors.html) to specify colours.\n", "* The string `--` indicates a dashed line, see other options [here](https://matplotlib.org/stable/gallery/lines_bars_and_markers/linestyles.html).\n", "* Capturing the output (artist) of `ax.plot` is not mandatory and not needed in most cases, but if we have it we can manipulate it (the line style **or even the data**) in the future (useful for animations!) For example:" ] }, { "cell_type": "code", "execution_count": null, "id": "15b758e1-4dde-45da-b2ff-1572dd8a0be9", "metadata": {}, "outputs": [], "source": [ "l.set_linewidth(0.5)" ] }, { "cell_type": "code", "execution_count": null, "id": "ca015d4c-c84f-44cc-b14f-9dc57c2f235c", "metadata": {}, "outputs": [], "source": [ "l.set_ydata(np.sin(4*x))" ] }, { "cell_type": "markdown", "id": "9f184f7d-2557-4be4-9385-472b23c515ee", "metadata": {}, "source": [ "* Some keywords have aliases: `color` β†’ `c`, `linewidth` β†’ `lw`, `linestyle` β†’ `ls`\n", "* The `plot` function also supports a third positional argument (after `x` and `y`) that can be used to specify colour and line style (as well as marker style), so the above is equivalent to `ax.plot(x, y1, 'r--', linewidth=4)`." ] }, { "cell_type": "markdown", "id": "7e70bef8-1037-49dc-89a7-889dc4b2a498", "metadata": {}, "source": [ "## More about figures and axes\n", "\n", "Let's create a figure with two sets of axes one on top of the other. We'll make the same line plot in both." ] }, { "cell_type": "code", "execution_count": null, "id": "6359214e-161f-4cf4-bb08-fa00b7410465", "metadata": {}, "outputs": [], "source": [ "fig, [ax1, ax2] = plt.subplots(nrows=2, figsize=(6.4, 6.4))\n", "func = lambda x: np.exp(x)/(np.sin(2*x)+2)\n", "ax1.plot(x, func(x))\n", "ax2.plot(x, func(x));" ] }, { "cell_type": "markdown", "id": "cbb81d58-53cf-44f4-a9df-4649e094aebe", "metadata": {}, "source": [ "* We used keyword arguments for `plt.subplots` to make two sets of axes in one column, and also change the figure size.\n", "* The `figsize` keyword argument takes a tuple of width and height in the Imperial \"inch\" unit (2.54 cm). The default figure size is 6.4 Γ— 4.8 of those units (`rcParams['figure.figsize']`), and the on-screen resolution is 100 pixels per \"inch\" (`rcParams['figure.dpi']`).\n", "* We drew two independent sets of axes. We can lock the x-axis by adding `sharex=True` to `plt.subplots`.\n", "* Of course, we can have bigger grids of subplots, and even more elaborate arrangements by using [GridSpec](https://matplotlib.org/stable/gallery/subplots_axes_and_figures/gridspec_multicolumn.html).\n", "\n", "This plot isn't very good because of the large range of y-values. Let's change the lower subplot such that the y-axis is logarithmic:" ] }, { "cell_type": "code", "execution_count": null, "id": "8e717429-f263-470f-9d27-8cf484e5a153", "metadata": {}, "outputs": [], "source": [ "ax2.set_yscale('log')" ] }, { "cell_type": "markdown", "id": "191d8566-4d84-4f57-bf8f-f1f65cfab096", "metadata": {}, "source": [ "By default, MPL leaves some margins around the data for determining axis limits (cf. `rcParams['axes.xmargin']`).\n", "\n", "We can manually set the limits to whatever we want like so:" ] }, { "cell_type": "code", "execution_count": null, "id": "725b84ef-d3dd-421d-9cd9-7918d9b7b167", "metadata": {}, "outputs": [], "source": [ "ax2.set_xlim(0, 10);" ] }, { "cell_type": "markdown", "id": "5c6a84fa-2e3f-461d-884f-6f0632ae7443", "metadata": {}, "source": [ "* If the x-axis is shared (`sharex=True`) then whatever limits we set for `ax2` will apply to `ax1` as well (and vice versa)!\n", "* `Axes` has convenience functions to create a line plot *and* simultaneously change the scaling to logarithmic, these are `semilogx`, `semilogy`, and `loglog`.\n", "\n", "If we want to change the spacing between the subplots, we can call `Figure.subplots_adjust` (cf. `Figure.tight_layout`)" ] }, { "cell_type": "code", "execution_count": null, "id": "d6fddea6-2bff-487f-80f3-e3c7c2476939", "metadata": {}, "outputs": [], "source": [ "fig.subplots_adjust(hspace=0.025)" ] }, { "cell_type": "markdown", "id": "b09cb982-1765-40a4-b736-b61856a4c6dc", "metadata": {}, "source": [ "## Scatter plots\n", "\n", "We can use `plt.plot` for simple scatter plots as well. We just have to make sure that there are markers but no line." ] }, { "cell_type": "code", "execution_count": null, "id": "07401111-f883-47df-9eea-9c09227d62fd", "metadata": {}, "outputs": [], "source": [ "x_peaks = np.arctan(-1/2) + np.arange(1, 6)*np.pi\n", "ax2.plot(x_peaks, func(x_peaks), linestyle='', marker='o');" ] }, { "cell_type": "markdown", "id": "432dce9b-d7d7-45b9-83e7-c81942603619", "metadata": {}, "source": [ "* There are quite a few markers to choose from (and you can make your own), see [here](https://matplotlib.org/stable/gallery/lines_bars_and_markers/marker_reference.html).\n", "* We could have created an equivalent plot with the third positional argument: `ax2.plot(x_peaks, func(x_peaks), 'o')`\n", "\n", "The `Axes` class has many more plotting functions! While `plot` can create a scatter plot, all markers have the same shape, size, and colour. We can use `scatter` instead if we want to represent additional properties visually. In the example below we use Pandas to retrieve a small dataset related to penguins, and create a scatter plot of bill length vs. flipper length, where the colour represents a third property (body mass)." ] }, { "cell_type": "code", "execution_count": null, "id": "47bb2323-13c5-4eab-be2c-8fd2da4d7063", "metadata": {}, "outputs": [], "source": [ "import pandas as pd\n", "df = pd.read_csv('https://raw.githubusercontent.com/allisonhorst/palmerpenguins/master/inst/extdata/penguins.csv')\n", "display(df)\n", "\n", "x = df['flipper_length_mm']\n", "y = df['bill_length_mm']\n", "prop = df['body_mass_g']" ] }, { "cell_type": "code", "execution_count": null, "id": "c7543cfe-f0b9-4089-8daf-6f316e169926", "metadata": {}, "outputs": [], "source": [ "fig, ax = plt.subplots()\n", "scatter_plot = ax.scatter(x, y, c=prop)\n", "ax.set_xlabel('Flipper length [mm]')\n", "ax.set_ylabel('Bill length [mm]')\n", "fig.colorbar(scatter_plot, label='Body mass [g]');" ] }, { "cell_type": "markdown", "id": "8386df33-b6a9-4547-9a35-1cbe0da95418", "metadata": {}, "source": [ "* This colour map is called *viridis* and is the default because it works well when printed in greyscale and is colourblind-friendly (\"monotonic luminance\"). It is also relatively perceptually uniform.\n", "* There are many more [options](https://matplotlib.org/stable/users/explain/colors/colormaps.html), and you can make your own.\n", " - Try passing `cmap='jet'` to `scatter`.\n", "* Instead of the `c` keyword argument, try using `s` to have the marker size represent the property\n", " - ⚠️ You will usually have to transform the property somehow to get a useful visualization." ] }, { "cell_type": "markdown", "id": "11ef1c38-fef2-42aa-a2ab-222e33000111", "metadata": {}, "source": [ "## Text and annotation\n", "\n", "We can place a text anywhere within the axes by calling the `text` function of the object (and there is also a `Figure.text` to place text in the figure, not associated with one axes/subplot object or another). In the example below we used `Axes.annotate` that is a bit more feature rich: it can place the text, but also shift it with respect to the point of interest, and draw and arrow to it." ] }, { "cell_type": "code", "execution_count": null, "id": "6904b6bc-30eb-4356-96b2-3992e2ff79f3", "metadata": {}, "outputs": [], "source": [ "i_maxmass = np.argmax(prop)\n", "x_maxmass, y_maxmass = x[i_maxmass], y[i_maxmass]\n", "ax.annotate(\n", " f'chonkiest\\npenguin\\n({0.001*prop[i_maxmass]} kg)', # What text to place\n", " (x_maxmass, y_maxmass), # What point to annotate\n", " xytext=(15, -80), textcoords='offset points', # Where to put the annotation\n", " arrowprops=dict(arrowstyle='->'), # Arrow properties\n", " bbox=dict(boxstyle='round', facecolor='w', alpha=0.5), # Box properties\n", " horizontalalignment='center' # Other text properties\n", ");" ] }, { "cell_type": "markdown", "id": "c94cb136-ac7d-4150-bb97-183fabd372e8", "metadata": {}, "source": [ "## More baic plot types" ] }, { "cell_type": "code", "execution_count": null, "id": "8c65243c-f582-4907-a275-3d1d7a8ecbc5", "metadata": {}, "outputs": [], "source": [ "x = np.arange(0, 10, 0.5)\n", "noise_envelope = np.sqrt((x+1)/10)\n", "np.random.seed(3)\n", "noise = np.random.randn(len(x))*noise_envelope\n", "y = np.cos(x) + noise\n", "fig, axs = plt.subplots(nrows=3, figsize=(6.4, 6.4), sharex=True)\n", "axs[0].errorbar(x, y, yerr=noise_envelope, linestyle='', marker='o', capsize=3)\n", "axs[0].set_title('Errorbar', y=0.8)\n", "axs[1].step(x, y)\n", "axs[1].set_title('Step', y=0.8)\n", "axs[2].bar(x, y, width=0.25)\n", "axs[2].set_title('Bar', y=0.8)\n", "axs[0].set_xlim(x[0], x[-1])\n", "fig.suptitle('More basic plot types')\n", "fig.subplots_adjust(hspace=0.05)" ] }, { "cell_type": "markdown", "id": "44b5fc31-ad62-46e3-8a30-c00c28746126", "metadata": {}, "source": [ "πŸ›ˆ A useful convenience function is `Axes.hist`, which calculates a histogram from a dataset and draws a bar (or step) plot. Generally speaking, MPL is not a package for data analysis or numerical calculations, but it does have a few convenience functions for statistics and spectral analysis.\n", "\n", "**Seaborn** is another library that uses MPL for data visualization. It provides a (even) higher-level API, that does both data analysis and plotting with fewer lines of code compared to MPL (check out Jarno's talk next week).\n", "\n", "## Visualizing 2D data\n", "\n", "The simplest way to visualize the values of a 2D array is by using the `Axes.imshow` function, assuming a regular grid. This function treats the array values as image pixels, and maps it into the axes based on the `extent` argument." ] }, { "cell_type": "code", "execution_count": null, "id": "5faf9121-eec1-4699-9ab7-e2930cb1387f", "metadata": {}, "outputs": [], "source": [ "x_ = np.linspace(-3, 3, 64)\n", "y_ = np.linspace(-3, 3, 64)\n", "x, y = np.meshgrid(x_, y_)\n", "z = 3*(1-x)**2*np.exp(-x**2-(y+1)**2) - 10*(x/5-x**3-y**5)*np.exp(-x**2-y**2) - (1/3)*np.exp(-(x+1)**2-y**2)\n", "\n", "fig, ax = plt.subplots()\n", "im = ax.imshow(z, origin='lower', extent=[-3, 3, -3, 3])\n", "fig.colorbar(im)\n", "ax.set_xlabel('x')\n", "ax.set_ylabel('y');" ] }, { "cell_type": "markdown", "id": "045144bb-2e93-48b0-b1a8-15013ec20309", "metadata": {}, "source": [ "Like with `scatter`, you can choose a different colour map by passing the `cmap` parameter.\n", "\n", "If the array is 3-dimensional, and the third dimension is size 3 or 4, this will be interpreted as a colour image (RGB or RGBA) and displayed accordingly.\n", "\n", "⚠️ `Axes.imshow` will show the array upside-down by default (with respect to how NumPy arrays are interpreted), use `origin='lower'` to change this behaviour.\n", "\n", "An alternative to `imshow` is `pcolormesh`, that supports non-uniform grids and have a syntax that's more similar to the basic plotting functions. However it is slower and may produce very large output files if a vector graphics format is used (such as PDF or SVG).\n", "\n", "You can also use `contour` or `contourf`:" ] }, { "cell_type": "code", "execution_count": null, "id": "6f40709c-809f-4144-b9d0-cf2c177185a4", "metadata": {}, "outputs": [], "source": [ "fig, ax = plt.subplots()\n", "contour = ax.contour(x, y, z, levels=12)\n", "ax.clabel(contour)\n", "ax.set_xlabel('x')\n", "ax.set_ylabel('y');" ] }, { "cell_type": "markdown", "id": "de34f6a7-8f37-44e1-be29-074158d0a25e", "metadata": {}, "source": [ "This plot looks squashed because by default it's adjusted to the size of the axes. We can force the aspect ratio like so:" ] }, { "cell_type": "code", "execution_count": null, "id": "196ba073-91fe-4c63-865e-e7971b19ac95", "metadata": {}, "outputs": [], "source": [ "ax.set_aspect('equal')" ] }, { "cell_type": "markdown", "id": "3509a00b-269f-48f2-aa6f-704e9f13779a", "metadata": {}, "source": [ "## 3D plotting\n", "\n", "Matplotlib can make basic and even reasonably advanced [3D plots](https://matplotlib.org/stable/gallery/mplot3d/index.html) (surfaces, wireframes, scatter, bars, etc.) It is not a full 3D rendering engine and lacks many features such as complex lighting and shadows, volumetric rendering, and advanced camera controls. Additionally it is not hardware accelerated, so will not handle large datasets as well as specialized tools.\n", "\n", "Here is a demonstration of plotting the 2D array from before as a surface:" ] }, { "cell_type": "code", "execution_count": null, "id": "044ce59b-77f4-4661-a02d-d505ebb68b2a", "metadata": {}, "outputs": [], "source": [ "fig = plt.figure()\n", "ax = fig.add_subplot(projection='3d')\n", "ax.plot_surface(x, y, z)\n", "ax.set_xlabel('x')\n", "ax.set_ylabel('y')\n", "ax.set_zlabel('z');" ] }, { "cell_type": "markdown", "id": "9f3c5405-c0e8-424e-ba4a-231705a48cac", "metadata": {}, "source": [ "## Animations\n", "\n", "An animation is just a sequence of still frames. As we saw throughout this lecture, once an artist is created, we can use various functions to modify it. The MPL *animation* module provides a useful class called `FuncAnimation`: you need to pass to it a function that updates the plot as a side effect, and the resultant `Animation` object can be saved or displayed on screen. Here is an example:" ] }, { "cell_type": "code", "execution_count": null, "id": "59012af2-78d2-459d-b899-7e468bcf045d", "metadata": {}, "outputs": [], "source": [ "import matplotlib.animation as animation\n", "\n", "frames = 60\n", "x = np.linspace(0, 16, 256)\n", "t = np.linspace(0, 2*np.pi, frames+1)[:-1]\n", "y1 = np.sin(x)*np.sin(t[0])\n", "y2 = np.cos(x)*np.cos(t[0])\n", "\n", "fig, ax = plt.subplots()\n", "l1, = ax.plot(x, y1, label='sine')\n", "l2, = ax.plot(x, y2, label='cosine')\n", "ax.legend(loc='upper left')\n", "ax.set_xlabel('x')\n", "ax.set_ylabel('y')\n", "ax.set_title('Trigonometric functions')\n", "text = ax.text(0, 0.65, 't = 0.00', bbox=dict(boxstyle='round', facecolor='w', alpha=0.5), fontfamily='monospace')\n", "\n", "def update(frame):\n", " y1 = np.sin(x)*np.sin(t[frame])\n", " y2 = np.cos(x)*np.cos(t[frame])\n", " l1.set_ydata(y1)\n", " l2.set_ydata(y2)\n", " text.set_text(f't = {t[frame]:.2f}')\n", "\n", "ani = animation.FuncAnimation(fig=fig, func=update, frames=frames, interval=3000/frames)\n", "\n", "# Show animation in notebook\n", "plt.close(fig)\n", "from IPython.display import HTML\n", "HTML(ani.to_html5_video())" ] }, { "cell_type": "markdown", "id": "52257124-abe9-499a-985e-1d5c76b752e1", "metadata": {}, "source": [ "* We created a figure much like we did before; we added two line plots, a legend, and a text.\n", "* Notice we passed `loc='upper left'` to `legend` (otherwise it will jump around in the animation depending on the line plots).\n", "* The `update` function recalculates the y values for both plots.\n", " - It updates the plot (`Line2D`) artists.\n", " - It updates the text (`Text`) artist.\n", "* There are several other ways to embed an animation in a notebook, this one works quite well.\n", " - We closed the figure with `plt.close` so the static figure doesn't show up in addition to the animation.\n", " - Beware of very large animations in a Jupyter notebook, you might want to just save to a file with `Animation.save`.\n", "\n", "## Interactivity\n", "\n", "In addition to the built-in interactive capabilities of the axes object, you can add [widgets](https://matplotlib.org/stable/gallery/widgets/index.html) such as buttons and sliders, capture events in the Python code and update the plot as needed.\n", "\n", "This probably shouldn't be your first choice to develop a UI for an application, but can be useful to interactively adjust parameters. The example below adds a button to the figure, clicking it calls the `update` function that, like before, changes the existing artists." ] }, { "cell_type": "code", "execution_count": null, "id": "c04a3ee3-2c18-4144-aa43-b68851401518", "metadata": {}, "outputs": [], "source": [ "from matplotlib.widgets import Slider\n", "fig, ax = plt.subplots()\n", "fig.subplots_adjust(bottom=0.2)\n", "x = np.linspace(0, 16, 256)\n", "y = np.sin(x)\n", "l, = ax.plot(x, y, linewidth=2)\n", "ax.set_title(r'$\\sin(\\omega x)$')\n", "ax.set_xlabel('x')\n", "ax.set_ylabel('y')\n", "\n", "def update(value):\n", " y = np.sin(x*value)\n", " l.set_ydata(y)\n", "\n", "ax_slider = fig.add_axes([0.3, 0.05, 0.4, 0.025])\n", "slider = Slider(ax_slider, r'$\\omega$', 0.5, 3, valinit=1)\n", "slider.on_changed(update);" ] }, { "cell_type": "code", "execution_count": null, "id": "c0f980e5-7eb0-4c2e-9f91-76845b791a1a", "metadata": {}, "outputs": [], "source": [] } ], "metadata": { "kernelspec": { "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.9.21" } }, "nbformat": 4, "nbformat_minor": 5 }