{ "cells": [ { "cell_type": "markdown", "id": "42235b78-7365-4921-8034-a7139a839952", "metadata": { "toc-hr-collapsed": true }, "source": [ "# Workshop \"Teaching Sciences and Engineering with Jupyter Notebooks\" 2023-2024\n", "\n", "Notebook by Cécile Hardebolle, 2023
\n", "Except where otherwise noted, the content of this notebook is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License.
\n", "\"Creative \n", "\n", "
\n", " How to use this notebook?
\n", " This notebook is made of text cells and code cells. The code cells have to be executed to see the result of the program.
To execute a cell, simply select it and click on the \"play\" button () in the tool bar just above the notebook, or type shift + enter.
It is important to execute the code cells in their order of appearance in the notebook.\n", "
" ] }, { "cell_type": "markdown", "id": "a2774ae7-2192-4acc-9b04-39a894706ab5", "metadata": {}, "source": [ "# Tutorial: creating interactive visualizations with Matplotlib\n", "\n", "This very short tutorial walks you through the steps for creating an interactive visualization with matplotlib. \n", "Please note that there are *multiple ways* to achieve the same result. This tutorial is just one possible example.\n", "\n", "Our goal in this tutorial is to obtain the interactive figure below, that represents a house (ok, it's a bit silly 😅 but it's just an example). \n", "The two sliders change the height of the roof and the width of the door.\n", "\n", "" ] }, { "cell_type": "markdown", "id": "a7ecbc64-f2c7-4066-ab9c-eeabe7675d06", "metadata": {}, "source": [ "# Technical setup\n", "\n", "## Importing libraries" ] }, { "cell_type": "markdown", "id": "2ca6f8ff-3012-47f2-8f91-f0c05c4ec0a7", "metadata": {}, "source": [ "We need to import two libraries: \n", "* matplotlib for plotting\n", "* ipywidgets for interaction elements such as sliders" ] }, { "cell_type": "code", "execution_count": null, "id": "178a7a7c-86dd-4bad-80ee-70c250708b82", "metadata": {}, "outputs": [], "source": [ "import matplotlib.pyplot as plt\n", "import ipywidgets as widgets" ] }, { "cell_type": "markdown", "id": "593bf85a-c041-442b-be27-d152f5ab9f05", "metadata": {}, "source": [ "## Choosing a \"backend\"\n", "\n", "Before starting to construct a figure, we need to tell matplotlib what we want to generate:\n", "* static images: `%matplotlib inline`\n", "* or a dynamic interactive visual: `%matplotlib widget`\n", "\n", "This setting changes the type of `backend` that matplotlib will use: \n", "* with the static backend, matplotlib will generate a new image each time we make a modification \n", "* with the interactive backend, only one image will be generated and all the actions and drawings will update the image - this is what we want to do in this tutorial." ] }, { "cell_type": "code", "execution_count": null, "id": "2f42895d-8a0a-41b9-aca7-118ed0c476cd", "metadata": {}, "outputs": [], "source": [ "# In this tutorial, we will create an interactive figure\n", "%matplotlib widget" ] }, { "cell_type": "markdown", "id": "c7a1a710-02f9-4ce0-b546-6422f2db7549", "metadata": {}, "source": [ "*Watchout: code written with one backend usually does not work with another backend!*" ] }, { "cell_type": "markdown", "id": "568a1cf2-9745-4396-8575-4d245dfa125a", "metadata": {}, "source": [ "# Data to generate the visualization\n", "To generate a visualization, we need some some data to plot.\n", "\n", "To simplify, we model the house by a rectangle and its roof by a triangle. \n", "These figures are drawn using lines defined by successive points represented by their x and y coordinates. \n", "\n", "For instance, to draw a rectangle, we plot a line defined by four points A, B, C and D, as in the figure below. \n", "\n", "\n", "\n", "To plot the line, we need to define two lists:\n", "* the x coordinates of these four points: [$x_A$, $x_B$, $x_C$, $x_D$]\n", "* the y coordinates of the same points: [$y_A$, $y_B$, $y_C$, $y_D$]" ] }, { "cell_type": "code", "execution_count": null, "id": "b2ec3458-425c-4d66-9f25-239471e2764b", "metadata": {}, "outputs": [], "source": [ "# We will plot a rectangle to model a house\n", "house_x = [2, 2, 4, 4]\n", "house_y = [0, 2, 2, 0]" ] }, { "cell_type": "markdown", "id": "fe94291e-3ff6-4d5d-baff-2e052c198a95", "metadata": {}, "source": [ "Then we do the same for the triangle that represents the roof." ] }, { "cell_type": "code", "execution_count": null, "id": "c22828f5-8b07-48de-bdc8-25b78bc89dcb", "metadata": {}, "outputs": [], "source": [ "# We will plot a triangle to model the roof\n", "roof_x = [2, 3, 4]\n", "roof_y = [2, 5, 2]" ] }, { "cell_type": "markdown", "id": "fea1da5d-f18d-48f4-8b4f-e24d3d1cb745", "metadata": {}, "source": [ "# Building the visualization" ] }, { "cell_type": "markdown", "id": "b0530b84-7d3d-4e65-9960-e222f434686f", "metadata": {}, "source": [ "## Turning off the output temporarily\n", "Before starting to build the figure, we first tell matplolib to wait until we tell him to show the figure where we want in the notebook. \n", "If we don't do that, it will start to display the figure right after the first instructions we write." ] }, { "cell_type": "code", "execution_count": null, "id": "9d65ead3-efc7-40f5-823a-306980328261", "metadata": {}, "outputs": [], "source": [ "# Turn the output off for the moment\n", "plt.ioff();" ] }, { "cell_type": "markdown", "id": "22c696c5-f1bb-48cc-b68a-286bea11396e", "metadata": {}, "source": [ "## Creating the components of the figure\n", "Now let's build the plot. It has two main components:\n", "* a figure, which is an overall container (usually \"invisible\")\n", "* one or more \"axes\" i.e. plots" ] }, { "cell_type": "code", "execution_count": null, "id": "f99f07ce-f750-493e-bac0-76427ac83ffb", "metadata": {}, "outputs": [], "source": [ "# Creation of the figure\n", "fig = plt.figure(num='Interactive figure', figsize=(6,4))\n", "\n", "# Creation of one subplot/axe - it will take position index number 1 in a grid of 1 row and 1 column, as described by (nrows, ncols, index)\n", "ax = fig.add_subplot(1,1,1)" ] }, { "cell_type": "markdown", "id": "19d36c78-ecb3-4f17-8199-a48437331805", "metadata": {}, "source": [ "## Plotting the data\n", "Once we have created these components, we can plot our data. \n", "The `plot` method of the `axe` object plots y versus x as lines and/or markers. It returns the resulting `line` object(s)." ] }, { "cell_type": "code", "execution_count": null, "id": "a061a2e0-1775-4752-8884-88bf00639903", "metadata": {}, "outputs": [], "source": [ "# First let's plot the \"house\" i.e. the rectangle\n", "ax.plot(house_x, house_y)\n", "\n", "# Then plot the roof/triangle, and get the resulting line, on which we will add interactivity later - NOTICE the syntax with the comma \"roof_line, =\"\n", "roof_line, = ax.plot(roof_x, roof_y)" ] }, { "cell_type": "markdown", "id": "69412705-47f2-40b3-9cd0-95422a0b6309", "metadata": {}, "source": [ "## Finally showing the figure\n", "Now let's show the resulting figure! \n", "For that we need to turn on again the interactive mode of matplotlib." ] }, { "cell_type": "code", "execution_count": null, "id": "d940f221-813c-4f52-8fa3-766fd0fa9b2f", "metadata": {}, "outputs": [], "source": [ "# Turn the output on\n", "plt.ion()\n", "\n", "# Show the figure\n", "display(fig.canvas)" ] }, { "cell_type": "markdown", "id": "425f9a44-f36b-417b-a9de-35e855bd5487", "metadata": {}, "source": [ "# Adding some interactivity \n", "Now we can add some interactivity to this plot. For that, we proceed in four steps:\n", "1. We create a slider, i.e. a button that allows the user to choose a value in a defined range." ] }, { "cell_type": "code", "execution_count": null, "id": "eadae473-19a9-4f18-b453-4aec54012015", "metadata": {}, "outputs": [], "source": [ "# We create a slider with values ranging from 2 to 10 in steps of .5, by default it will be set on value 5\n", "roof_widget = widgets.FloatSlider(min=2, max=10, step=0.5, value=5, description='Roof height:')" ] }, { "cell_type": "markdown", "id": "37549dfa-026c-4ee2-9c45-5e55b06f84b8", "metadata": {}, "source": [ "2. We create a function that will be called when the slider is moved and will update the figure: \n", " it will change the coordinates of the points in the line that represents the roof, using the value indicated by the slider." ] }, { "cell_type": "code", "execution_count": null, "id": "93c29bff-d13f-477f-ab8d-9723856472b1", "metadata": {}, "outputs": [], "source": [ "# This function will be called when the slider is moved\n", "def roof_event_handler(change):\n", " # It allows us to retrieve the new value of the slider\n", " newposition = change.new\n", "\n", " # Then we can update the y coordinates of the points in the roof line - this is the important part of the function!!\n", " # Note: to change the height of the roof, we only change the y coordinate of the middle point (top of the triangle), the first and last points do not change\n", " roof_line.set_ydata([2, newposition, 2])\n", " \n", " # Finally we tell the figure to draw the changed parts\n", " fig.canvas.draw_idle()" ] }, { "cell_type": "markdown", "id": "bfc19f67-fd2a-4d99-a138-3e84c25a962b", "metadata": {}, "source": [ "3. We link the slider to the callback function" ] }, { "cell_type": "code", "execution_count": null, "id": "c7155220-f5f2-40f4-aad6-38d055441646", "metadata": {}, "outputs": [], "source": [ "# Finally we link the widget to the callback function \n", "roof_widget.observe(roof_event_handler, names='value')" ] }, { "cell_type": "markdown", "id": "3242d42a-706b-4040-bf08-a2d118829ab4", "metadata": {}, "source": [ "4. And then we display both the figure and the slider" ] }, { "cell_type": "code", "execution_count": null, "id": "9ebd1106-589f-46ad-b7cd-bfce1db0744a", "metadata": {}, "outputs": [], "source": [ "# Display the figure again, and the widget below (no need to turn the interactive mode on again since we did it earlier)\n", "display(fig.canvas, roof_widget)" ] }, { "cell_type": "markdown", "id": "03b9a626-ee0a-4bec-ba57-d2691252d881", "metadata": {}, "source": [ "Note that the slider udpates both the figure here and also the figure above. \n", "This is the work of the interactive backend!" ] }, { "cell_type": "markdown", "id": "e6c31f99-06b2-42a5-988f-a8e81951b6e2", "metadata": {}, "source": [ "---\n", "\n", "# Your turn now!" ] }, { "cell_type": "markdown", "id": "b4c41719-fe79-4c76-ba62-68e5fbb3edc1", "metadata": {}, "source": [ "
\n", " Activity
\n", "\n", "Add a line to the plot that draws a door to the house. \n", "Then create a slider that will let the user choose the width of the door. \n", " \n", "
" ] }, { "cell_type": "markdown", "id": "8a9f7b39-9bf7-48e4-aedf-a4583815794a", "metadata": {}, "source": [ "Here are the steps you need to follow:\n", "1. First define the x and y coordinates of the line that will represent the door\n", "1. Plot the line, retrieve the result\n", "1. Create a slider\n", "1. Create a function that will update the coordinates of the points of the door line depending on the value of the slider\n", "1. Link the slider to the function\n", "1. Display the figure and the sliders" ] }, { "cell_type": "code", "execution_count": null, "id": "f0f60b17-d9cb-4b51-93b4-d472edf2cd2e", "metadata": {}, "outputs": [], "source": [ "# Your code here...\n" ] }, { "cell_type": "markdown", "id": "206b7ae1-02b9-44d0-9af9-4472df30adbe", "metadata": {}, "source": [ "
\n", "\n", "**Solution** - You can see *one possible solution* by clicking on the \"...\" below.\n", " \n", "
" ] }, { "cell_type": "code", "execution_count": null, "id": "15d0e85e-abc0-4ff1-af59-47eb4c19869b", "metadata": { "jupyter": { "source_hidden": true }, "tags": [] }, "outputs": [], "source": [ "# First define the x and y coordinates of the door\n", "door_x = [2.25, 2.25, 2.50, 2.50] # originally, the door has a width of 0.25, i.e. the points on the right side are at x_left + 0.25\n", "door_y = [0, 1, 1, 0]\n", "\n", "# Draw the door on the plot and get the resulting line (to be able to update it with the slider)\n", "door_line, = ax.plot(door_x, door_y)\n", "\n", "# Create a widget for the width of the door, which starts at 0.25 and can go up to 1.5 in steps of 0.05\n", "door_widget = widgets.FloatSlider(min=0.25, max=1.5, step=0.05, value=0.25, description='Door width:')\n", "\n", "# This function will be called when the door slider is moved\n", "def door_event_handler(change):\n", " # It allows us to retrieve the new value of the slider\n", " newwidth = change.new\n", "\n", " # Then we can change the points of the door line - in this case we only change the x coordinates of the points on the right side of the door\n", " door_line.set_xdata([2.25, 2.25, 2.25+newwidth, 2.25+newwidth])\n", " \n", " # Finally we tell the figure to draw the changed parts\n", " fig.canvas.draw_idle()\n", " \n", "\n", "# Finally we link the widget to the callback function \n", "door_widget.observe(door_event_handler, names='value')\n", "\n", "# Let's display again the whole figure with the two sliders\n", "display(fig.canvas, roof_widget, door_widget)" ] }, { "cell_type": "markdown", "id": "9690331e-446d-4062-97d0-069276e335bf", "metadata": {}, "source": [ "---\n", "\n", "# Additional resources\n", "\n", "More on figures and axes of Matplotlib: \n", "https://medium.com/@kapil.mathur1987/matplotlib-an-introduction-to-its-object-oriented-interface-a318b1530aed\n", "\n", "Using the widgets to add interactivity: \n", "https://ipywidgets.readthedocs.io/en/latest/examples/Widget%20Events.html\n", "\n", "Details about the backends of matplotlib: \n", "https://matplotlib.org/3.4.3/users/interactive.html" ] } ], "metadata": { "kernelspec": { "display_name": "Python3", "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.11.4" } }, "nbformat": 4, "nbformat_minor": 5 }