{ "metadata": { "name": "" }, "nbformat": 3, "nbformat_minor": 0, "worksheets": [ { "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "
\n", " \n", "
\n", "\n", "# Exploratory Computing with Python\n", "*Developed by Mark Bakker*\n", "## Advanced Topic Notebook 4: Interactive graphics with `matplotlib` widgets" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "In this Notebook, we learn how to add interactivity to graphs using `matplotlib` widgets. A widget (phonetic: \u02c8w\u026ad\u0292\u026at') is an interactive part of a user interface. Examples are *radio buttons*, where you can select only one button at a time, *check buttons*, where you can select multiple buttons at a time, and *sliders* which you can, well, slide to change something in the graph." ] }, { "cell_type": "code", "collapsed": false, "input": [ "%pylab qt" ], "language": "python", "metadata": {}, "outputs": [ { "output_type": "stream", "stream": "stdout", "text": [ "Populating the interactive namespace from numpy and matplotlib\n" ] } ], "prompt_number": 1 }, { "cell_type": "markdown", "metadata": {}, "source": [ "###Creating a widget\n", "The process for creating a widget is the same for each widget:\n", "\n", "1. Create a figure with the desired graph and make sure the components of the graph that you want to change are stored as objects\n", "2. Add a new axis to a graph for the widget. \n", "3. Create the widget and add it to the appropriate axis.\n", "4. Write a function that is called when the state of the widget is changed (i.e., a radio button is selected or the slider is moved).\n", "5. Tell the widget to call this function when its state is changed.\n", "6. When all is done, call `show()`\n", "\n", "Widgets need to be imported from the `matplotlib.widgets` package. All widgets are listed here. In this Notebook, we will use radio buttons and sliders." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "###Radio buttons\n", "Consider the graph of a circle. We will add radio buttons to change the radius of the circle. The code and an image of the interactive window are shown below.\n", "\n", "In *step 1* we create a figure, add an an axis, and add a circle to the axis. The circle object is stored as `c1`. The axis is created with the `axes` command, and the aspect ratio is set with the keyword argument `aspect='equal'` so that a circle looks like a circle and not an ellipse. Finally, the data limits of the axes are set. This is important as `matplotlib` won't do that for you when you add a patch. \n", "\n", "In *step 2* we create a second axis that will contain the radio buttons; the axis is called `rax`. The aspect ratio is again set to `'equal'` so that the buttons look like cirles (rather than ellipses), and the title of the axis is set to `Radius`.\n", "\n", "In *step 3* a `RadioButtons` object is created and called `radio`. The `RadioButtons` class takes two arguments: the axis to which the radio buttons are added (here it is `rax`), and a list with the labels of the buttons. We create three buttons with labels 1, 2, and 6. The labels need to be given as a list of strings. Two additional keyword arguments are provided: `active=1` tells the `RadioButtons` class that upon creation button number 1 is active (the first button is number *zero*) as the circle has a radius $R=2$ when we create it. The keyword argument `activecolor='r'` tells the `RadioButtons` class to use the color red for the active button. \n", "\n", "In *step 4* the function `setradius` is defined (we can choose whatever name we find appropriate). The function takes one argument, which is the label of one of the radio buttons. When the function is called, it sets the radius of circle `c1` to the value of the label of the radio button. Note that the label of the radio button is a string, so we have to convert it to a float first. The function ends by calling the `draw()` function that redraws the figure with a circle with the new radius. \n", "\n", "In *step 5*, the `radio` object is told that when one of the radio buttons is clicked it should call the `setradius` function. The `on_clicked` function is part of the `RadioButtons` class. \n", "\n", "Finally, in *step 6* we call `show()` to show the figure.\n", "\n", "When you run the code below, a seperate figure window is created in which you can change the radius of the circle interactively by clicking on the radio buttons. A still image of what you will see is shown below the code." ] }, { "cell_type": "code", "collapsed": false, "input": [ "from pylab import *\n", "from matplotlib.widgets import RadioButtons\n", "from matplotlib.patches import Circle\n", "# step 1\n", "fig = figure(figsize=(12,8))\n", "ax1 = fig.add_axes([0.4,0.1,0.4,0.8], aspect='equal')\n", "c1 = Circle(xy=(0,0),radius=2)\n", "ax1.add_patch(c1)\n", "ax1.set_xlim(-5,5)\n", "ax1.set_ylim(-5,5)\n", "# step 2\n", "rax = fig.add_axes([0.1,0.45,0.2,0.2], aspect='equal', title='Radius')\n", "# step 3\n", "radio = RadioButtons( rax, ['1','2','6'], active=1, activecolor='r')\n", "# step 4\n", "def setradius(label):\n", " c1.set_radius(float(label))\n", " draw()\n", "# step 5\n", "radio.on_clicked(setradius)\n", "# step 6\n", "show()" ], "language": "python", "metadata": {}, "outputs": [], "prompt_number": 2 }, { "cell_type": "markdown", "metadata": {}, "source": [ "
\n", " \n", "
" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "###Execise 1. Radio buttons to set color\n", "Add a second set of radio buttons to change the color of the circle shown in the previous example. You may want to relocate the radio buttons that set the radius of the circle to a more convenient place on the graph. Provide at least three options for setting the color of the circle. Note that a `Circle` object has a `set_color` function." ] }, { "cell_type": "code", "collapsed": false, "input": [], "language": "python", "metadata": {}, "outputs": [], "prompt_number": 2 }, { "cell_type": "markdown", "metadata": {}, "source": [ "Answers to Exercise 1" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "###Changing the shape of a line\n", "Adding a line is similar to adding a circle. A line is added as a `Line2D` object. In the example below, we create a straight line $y=ax+b$. A `Line2D` object is created and added to the axis and the limits of the axes are set. Then we add radio buttons to change the slope of the line. A still image of the interactive window is shown below the code." ] }, { "cell_type": "code", "collapsed": false, "input": [ "from pylab import *\n", "from matplotlib.widgets import RadioButtons\n", "from matplotlib.lines import Line2D\n", "\n", "# Step 1: Create figure with the line\n", "fig = figure(figsize=(12,8))\n", "ax = fig.add_axes([0.4,0.1,0.5,0.8],aspect='equal')\n", "a = 1\n", "b = 0\n", "x = linspace(0,20,10)\n", "y = a * x + b\n", "line1 = Line2D(x,y)\n", "ax.add_line(line1)\n", "xlim(0,20)\n", "ylim(0,20)\n", "\n", "# Radio buttons for slope\n", "ax1 = fig.add_axes([0.1,0.65,0.2,0.2], aspect='equal', title='slope') # step 2\n", "slope = RadioButtons( ax1, ['0.5','1','2'], active=1, activecolor='r') # step 3\n", "def setslope(label): # step 4\n", " a = float(label)\n", " y = a * x + b\n", " line1.set_ydata(y)\n", " draw()\n", "slope.on_clicked(setslope) # step 5\n", "\n", "show() # step 6" ], "language": "python", "metadata": {}, "outputs": [], "prompt_number": 3 }, { "cell_type": "markdown", "metadata": {}, "source": [ "
\n", " \n", "
" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "In the function `setslope` the slope is set to the value selected with the radio buttons. But, since the slope `a` is set inside a function, it is not set outside the function. After all, any variable created inside a function only exists inside the function and not outside. But in this case, we want to change the slope `a` outside the function as well. For that we define the variable `a` to be global inside the `setslope` function. We also add a second set of radio buttons to set the $y$-intercept `b`, where `b` is defined as `global` inside the `setintercept` function." ] }, { "cell_type": "code", "collapsed": false, "input": [ "from pylab import *\n", "from matplotlib.widgets import RadioButtons\n", "from matplotlib.lines import Line2D\n", "\n", "# Create figure with line\n", "fig = figure(figsize=(12,8))\n", "ax = fig.add_axes([0.4,0.1,0.5,0.8],aspect='equal')\n", "a = 1\n", "b = 0\n", "x = linspace(0,20,10)\n", "y = a * x + b\n", "line1 = Line2D(x,y)\n", "ax.add_line(line1)\n", "xlim(0,20)\n", "ylim(0,20)\n", "\n", "# Radio buttons for slope and intercept\n", "ax1 = fig.add_axes([0.1,0.65,0.2,0.2], aspect='equal', title='slope') # step 2\n", "ax2 = fig.add_axes([0.1,0.25,0.2,0.2], aspect='equal')\n", "slope = RadioButtons( ax1, ['0.5','1','2'], active=1, activecolor='r') # step 3\n", "intercept = RadioButtons( ax2, ['0','5','10'], active=0, activecolor='r')\n", "def setslope(label): # step 4\n", " global a\n", " a = float(label)\n", " y = a * x + b\n", " line1.set_ydata(y)\n", " draw()\n", "def setintercept(label):\n", " global b\n", " b = float(label)\n", " y = a * x + b\n", " line1.set_ydata(y)\n", " draw()\n", "slope.on_clicked(setslope) # step 5\n", "intercept.on_clicked(setintercept)\n", "\n", "show() # step 6" ], "language": "python", "metadata": {}, "outputs": [], "prompt_number": 4 }, { "cell_type": "markdown", "metadata": {}, "source": [ "###Exercise 2. Deflection of a beam\n", "Consider the elastic deflection of a beam with a uniform load $q$. The beam is supported by two simple supports (see figure below). The formula for the shape $y(x)$ of the deformed beam is\n", "\n", "$y = -\\frac{qx}{24EI} (L^3 - 2Lx^2 + x^3)$\n", "\n", "where $E$ is the elasticity modulus, $I$ is the area moment of intertia, and $L$ is the length of the beam. Make a graph using $q=100$ N/mm, $L=5000$ mm, $I= 1067\\cdot 10^6$ mm$^4$ (i.e., a beam of width 200 mm and height 400 mm), and the elasticity modulus of wood $E=11000$ N/mm$^2$. Set the $x$-limits of the graph from $0$ to $5000$, and the $y$-limits of the graph from $-100$ to $0$ (you won't see anything if you forget to set the limits, as they are not updated automatically in the OO modus). Add two sets of radiobuttons. In the first set of radiobuttons, the type of material can be selected: wood ($E=11000$ N/mm$^2$), concrete ($E=35000$ N/mm$^2$), or aluminum ($E=71000$ N/mm$^2$). In the second set of radiobuttons the load can be selected: $50$, $100$ or $200$ N/mm. Make sure that once a radiobutton is selected, the shape of the deflected beam changes accordingly.\n", "\n", "\"deflection\"\n" ] }, { "cell_type": "code", "collapsed": false, "input": [], "language": "python", "metadata": {}, "outputs": [], "prompt_number": 4 }, { "cell_type": "markdown", "metadata": {}, "source": [ "Answers to Exercise 2" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "###Slider\n", "Consider again the graph of a circle but now with an initially horizontal line through the center of the circle. We will add a slider to the graph that allows the user to rotate the circle from -180$^\\circ$ to +180$^\\circ$. The proces consists again of the six steps defined at the top of this Notebook. In *step 1* we create the graph. The line is added as a Line2D class called `line1` and has only two points. The first point is $(x,y)=(-R\\cos(\\alpha),-R\\sin(\\alpha))$, the second point is $(x,y)=(R\\cos(\\alpha),R\\sin(\\alpha))$; initially the angle is $\\alpha=0$ and the radius is $R=3$. In *step 2* we create the axis to which we will add the slider. In *step 3* we create the slider and call it `angleslider`. The `Slider` class takes four input arguments: the axis to add the slider to, the name that is printed next to the slider, the minimum value and the maximum value. There are also a number of keyword arguments (use `Slider?` after you import it). We use one here: we set the initial value of the slider to zero using the `valinit` keyword. In *step 4* we define an update function `setangle` that will be called when the value of the slider is changed. It takes as argument the value of the slider, it changes the $x$ and $y$ data of the line, and it draws the updated figure. In *step 5* we tell the slider to call the `setangle` function when the value is changed using the `on_changed` function. And finally in *step 6* we show the figure. A still image of a more complicated version of this figure is shown below Exercise 4." ] }, { "cell_type": "code", "collapsed": false, "input": [ "from pylab import *\n", "from matplotlib.widgets import Slider\n", "from matplotlib.patches import Circle\n", "\n", "# Step 1: Create figure with circle and line\n", "fig = figure(figsize=(6,8))\n", "ax1 = fig.add_axes([0.1,0.3,0.8,0.6], aspect='equal')\n", "R = 3\n", "c1 = Circle(xy=(0,0),radius=R,fc='violet')\n", "ax1.add_patch(c1)\n", "angle = 0\n", "line1 = Line2D(xdata = [-R*cos(angle),R*cos(angle)],\\\n", " ydata = [-R*sin(angle),R*sin(angle)],color='k')\n", "ax1.add_line(line1)\n", "xlim(-5,5)\n", "ylim(-5,5)\n", "# Step 2: Create axis for slider\n", "axslider = fig.add_axes([0.2,0.15,0.6,0.05])\n", "# Step 3: Create slider and add to axis\n", "angleslider = Slider(axslider, 'Angle', -180, 180, valinit=0)\n", "# Step 4: Function to call when slider is changed\n", "def setangle(val):\n", " angle = val * pi/180\n", " line1.set_xdata([-R*cos(angle),R*cos(angle)])\n", " line1.set_ydata([-R*sin(angle),R*sin(angle)])\n", " draw()\n", "# Step 5: Tell the angleslider what function to call when its value has changed\n", "angleslider.on_changed(setangle)\n", "# Step 6: Call show()\n", "show()" ], "language": "python", "metadata": {}, "outputs": [], "prompt_number": 5 }, { "cell_type": "markdown", "metadata": {}, "source": [ "###Exercise 3. Sliders\n", "*Part 1* Add a small blue circle with radius 0.2 at the intersection of the line and circle (see figure below but without the second slider). Initially, when $\\alpha=0$, the center of the small circle is at $(x,y)=(R,0)$. Make sure that the small circle rotates with the line when the angle is changed with the slider. " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "*Part 2*\n", "Add a second slider to the graph you created for Part 1. The second slider allows the user to change the radius of the circle between 2 and 4. Note that you have to create a second slider axis under *step 2*, a second slider under *step 3*, a second update function under *step 4*, and you need to tell the new slider which function to call when it is changed under *step 5*. The current `setangle` function uses the variable `R` as the radius of the large circle. In this new implementation, the radius is set by the new slider. The current value of a slider is stored in the `val` attribute of the slider. Hence, if your slider is called `slidername`, then `slidername.val` is the current value of the slider. When you write your code, it is important that you first define both sliders under *step 3*, as the update functions you define in *step 4* need to have access to the value of the other slider (either the radius or the angle). When you are done, you graph should look something like the figure below (and the sliders should work, or course). \n", "\n", "\"circle" ] }, { "cell_type": "code", "collapsed": false, "input": [], "language": "python", "metadata": {}, "outputs": [], "prompt_number": 5 }, { "cell_type": "markdown", "metadata": {}, "source": [ "Answers to Exercise 3" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "###Answers to the exercises" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Answers to Exercise 1" ] }, { "cell_type": "code", "collapsed": false, "input": [ "from pylab import *\n", "from matplotlib.widgets import RadioButtons\n", "from matplotlib.patches import Circle\n", "\n", "# Create figure with circle\n", "fig = figure(figsize=(12,8))\n", "ax1 = fig.add_axes([0.4,0.1,0.5,0.8], aspect='equal')\n", "c1 = Circle(xy=(0,0),radius=2,color='blue')\n", "ax1.add_patch(c1)\n", "xlim(-5,5)\n", "ylim(-5,5)\n", "\n", "# Radio buttons for radius of circle\n", "rax = axes([0.1,0.65,0.2,0.2], aspect='equal', title='Radius')\n", "radio = RadioButtons( rax, ['1','2','6'], active=1, activecolor='r')\n", "def setradius(label):\n", " c1.set_radius(float(label))\n", " draw()\n", "radio.on_clicked(setradius)\n", "\n", "# Radio buttons for color of circle\n", "cax = axes([0.1,0.25,0.2,0.2], aspect='equal', title='Color')\n", "color = RadioButtons( cax, ['violet','blue','green','black'], active=1, activecolor='r')\n", "def setcolor(label):\n", " c1.set_color(label)\n", " draw()\n", "color.on_clicked(setcolor)\n", "\n", "show()" ], "language": "python", "metadata": {}, "outputs": [], "prompt_number": 6 }, { "cell_type": "markdown", "metadata": {}, "source": [ "Back to Exercise 1\n", "\n", "Answers to Exercise 2" ] }, { "cell_type": "code", "collapsed": false, "input": [ "from pylab import *\n", "from matplotlib.widgets import RadioButtons\n", "from matplotlib.lines import Line2D\n", "\n", "fig = figure()\n", "ax1 = fig.add_axes([0.1,0.4,0.8,0.5])\n", "E = 11e3\n", "I = 1067e6\n", "q = 100\n", "L = 5000\n", "x = linspace(0,L,100)\n", "d = -q * x / (24*E*I) * (L**3 - 2*L*x**2 + x**3)\n", "line = Line2D(xdata=x,ydata=d)\n", "ax1.add_line(line)\n", "xlabel('x (mm)')\n", "ylabel('deflection (mm)')\n", "xlim(0,L)\n", "ylim(-100,0)\n", "\n", "ax2 = fig.add_axes([0.05,0.05,0.2,0.2],aspect='equal',title='material')\n", "ax3 = fig.add_axes([0.55,0.05,0.2,0.2],aspect='equal',title='qload')\n", "\n", "Emod = RadioButtons( ax2, ['wood','concrete','aluminum'], active=0, activecolor='r')\n", "qload = RadioButtons( ax3, ['50','100','200'], active=1, activecolor='r')\n", "def setEmod(label):\n", " global E\n", " if label == 'wood': E = 11e3\n", " if label == 'concrete': E = 35e3\n", " if label == 'aluminum': E = 71e3\n", " d = -q * x / (24*E*I) * (L**3 - 2*L*x**2 + x**3)\n", " line.set_ydata(d)\n", " draw()\n", "def setqload(label):\n", " global q\n", " q = float(label)\n", " d = -q * x / (24*E*I) * (L**3 - 2*L*x**2 + x**3)\n", " line.set_ydata(d)\n", " draw()\n", "Emod.on_clicked(setEmod)\n", "qload.on_clicked(setqload)\n", "\n", "show()" ], "language": "python", "metadata": {}, "outputs": [], "prompt_number": 7 }, { "cell_type": "markdown", "metadata": {}, "source": [ "Back to Exercise 2\n", "\n", "Answers to Exercise 3" ] }, { "cell_type": "code", "collapsed": false, "input": [], "language": "python", "metadata": {}, "outputs": [], "prompt_number": 7 }, { "cell_type": "code", "collapsed": false, "input": [ "from pylab import *\n", "from matplotlib.widgets import Slider\n", "from matplotlib.patches import Circle\n", "\n", "fig = figure(figsize=(6,8))\n", "ax1 = fig.add_axes([0.1,0.3,0.8,0.6], aspect='equal')\n", "R = 3\n", "c1 = Circle(xy=(0,0),radius=R,fc='violet')\n", "ax1.add_patch(c1)\n", "angle = 0\n", "l1 = Line2D(xdata = [-R*cos(angle),R*cos(angle)],\\\n", " ydata = [-R*sin(angle),R*sin(angle)],color='k')\n", "ax1.add_line(l1)\n", "c2 = Circle(xy=(R*cos(angle),R*sin(angle)),radius=0.2,fc='b')\n", "ax1.add_patch(c2)\n", "xlim(-5,5)\n", "ylim(-5,5)\n", "\n", "axslider = fig.add_axes([0.2,0.15,0.6,0.05])\n", "angleslider = Slider(axslider, 'Angle', -180, 180, valinit=0)\n", "axslider2 = fig.add_axes([0.2,0.05,0.6,0.05])\n", "angleslider2 = Slider(axslider2, 'Radius', 2, 4, valinit=3)\n", "\n", "def update(val):\n", " angle = val * pi/180\n", " R = angleslider2.val\n", " l1.set_xdata([-R*cos(angle),R*cos(angle)])\n", " l1.set_ydata([-R*sin(angle),R*sin(angle)])\n", " c2.center = R*cos(angle),R*sin(angle)\n", " draw()\n", "\n", "def update2(R):\n", " angle = angleslider.val * pi/180\n", " l1.set_xdata([-R*cos(angle),R*cos(angle)])\n", " l1.set_ydata([-R*sin(angle),R*sin(angle)])\n", " c2.center = R*cos(angle),R*sin(angle)\n", " c1.radius = R\n", " draw()\n", "\n", "angleslider.on_changed(update)\n", "angleslider2.on_changed(update2)\n", "\n", "show()" ], "language": "python", "metadata": {}, "outputs": [], "prompt_number": 8 }, { "cell_type": "markdown", "metadata": {}, "source": [ "Back to Exercise 3" ] }, { "cell_type": "code", "collapsed": false, "input": [], "language": "python", "metadata": {}, "outputs": [], "prompt_number": 8 } ], "metadata": {} } ] }