{ "metadata": { "name": "02_Key_Bindings" }, "nbformat": 3, "nbformat_minor": 0, "worksheets": [ { "cells": [ { "cell_type": "heading", "level": 1, "metadata": {}, "source": [ "Adding Interactivity: Key Bindings and Callbacks" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We saw before that plot elements can be adjusted dynamically. The creation of\n", "an interactive plots is a short step from this: we simply need to add callbacks to\n", "the plot so that mouse and keyboard actions in the figure window lead to the plot\n", "elements being updated.\n", "\n", "This is a little known aspect of matplotlib: the presence of key bindings and callback\n", "functions which can be bound to various actions.\n", "The main interface to these is through the ``plt.connect``\n", "interface. You can see the various events by looking at the documentation string\n", "of the ``plt.connect`` function:" ] }, { "cell_type": "code", "collapsed": false, "input": [ "# start in non-inline mode, so we can use interactive plots\n", "%pylab" ], "language": "python", "metadata": {}, "outputs": [] }, { "cell_type": "code", "collapsed": false, "input": [ "print help(plt.connect)" ], "language": "python", "metadata": {}, "outputs": [] }, { "cell_type": "heading", "level": 2, "metadata": {}, "source": [ "Keyboard Callbacks" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Using these sorts of callbacks is extremely straightforward. Let's start with\n", "looking at binding key-presses to actions." ] }, { "cell_type": "code", "collapsed": false, "input": [ "import sys\n", "fig, ax = plt.subplots()\n", "\n", "def on_key_press(event):\n", " print event.key\n", " sys.stdout.flush()\n", " \n", "fig.canvas.mpl_connect('key_press_event', on_key_press)" ], "language": "python", "metadata": {}, "outputs": [] }, { "cell_type": "markdown", "metadata": {}, "source": [ "You might notice that if you hit the ``s`` key here, you get some somewhat\n", "unexpected behavior. For example, if you press the ``g`` key, you'll see\n", "that the grid lines toggle on or off. If you press the ``s`` key, you'll\n", "see an interactive figure save window appear.\n", "\n", "These key-bound shortcuts are activated by default, but can be turned off\n", "with the following command:" ] }, { "cell_type": "code", "collapsed": false, "input": [ "fig.canvas.mpl_disconnect(fig.canvas.manager.key_press_handler_id)" ], "language": "python", "metadata": {}, "outputs": [] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Here's the last code snippet repeated all at once:" ] }, { "cell_type": "code", "collapsed": false, "input": [ "import sys\n", "fig, ax = plt.subplots()\n", "\n", "def on_key_press(event):\n", " print event.key\n", " sys.stdout.flush()\n", "\n", "fig.canvas.mpl_disconnect(fig.canvas.manager.key_press_handler_id)\n", "fig.canvas.mpl_connect('key_press_event', on_key_press)" ], "language": "python", "metadata": {}, "outputs": [] }, { "cell_type": "heading", "level": 3, "metadata": {}, "source": [ "Quick Example: Changing a Line Color" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Let's create a slightly more interesting callback function. We can\n", "use the same line updating code to dynamically adjust the color\n", "of a line:" ] }, { "cell_type": "code", "collapsed": false, "input": [ "fig, ax = plt.subplots()\n", "x = np.linspace(0, 10, 1000)\n", "line, = ax.plot(x, np.sin(x))\n", "\n", "def on_key_press(event):\n", " # If the key is one of the shorthand color notations, set the line color\n", " if event.key in 'rgbcmyk':\n", " line.set_color(event.key)\n", "\n", " fig.canvas.draw()\n", "\n", "fig.canvas.mpl_disconnect(fig.canvas.manager.key_press_handler_id)\n", "fig.canvas.mpl_connect('key_press_event', on_key_press)" ], "language": "python", "metadata": {}, "outputs": [] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**Exercise:** modify the above function so that if the user presses a number,\n", "the *linewidth* of the line is changed to that number." ] }, { "cell_type": "code", "collapsed": false, "input": [], "language": "python", "metadata": {}, "outputs": [] }, { "cell_type": "heading", "level": 2, "metadata": {}, "source": [ "Mouse Callbacks" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Just as we bound keyboard callbacks to actions, we can bind mouse buttons and movements\n", "to actions. The mouse events store the **button** as an integer ID (Note that the button press event\n", "stores both the **pixel** value of the event location, as well as the **data** value\n", "of the event location." ] }, { "cell_type": "code", "collapsed": false, "input": [ "import sys\n", "fig, ax = plt.subplots()\n", "\n", "def on_button_press(event):\n", " print dir(event)\n", " print \"Button:\", event.button\n", " print \"Figure coordinates:\", event.x, event.y\n", " print \"Data coordinates:\", event.xdata, event.ydata\n", " sys.stdout.flush()\n", " \n", "fig.canvas.mpl_connect('button_press_event', on_button_press)" ], "language": "python", "metadata": {}, "outputs": [] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Another interesting mouse event is the ``motion_notify_event``. Here, rather than the\n", "callback being activated whenever the mouse button is clicked, it is activated whenever\n", "the mouse is moved." ] }, { "cell_type": "code", "collapsed": false, "input": [ "import sys\n", "fig, ax = plt.subplots()\n", "\n", "def on_mouse_move(event):\n", " print \"Data coordinates:\", event.xdata, event.ydata\n", " sys.stdout.flush()\n", " \n", "fig.canvas.mpl_connect('motion_notify_event', on_mouse_move)" ], "language": "python", "metadata": {}, "outputs": [] }, { "cell_type": "heading", "level": 2, "metadata": {}, "source": [ "Quick Example: Manipulating a Polygon" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Mouse callbacks allow us to do some interesting things. For example, here we'll\n", "manipulate the color of a polygon by clicking on it:" ] }, { "cell_type": "code", "collapsed": false, "input": [ "fig, ax = plt.subplots()\n", "ax.set_xlim(-1, 2)\n", "ax.set_ylim(-1, 2)\n", "rect = plt.Rectangle((0, 0), 1, 1, fc=np.random.random(3))\n", "ax.add_patch(rect)\n", "\n", "def on_click(event):\n", " x, y = event.xdata, event.ydata\n", " if (x > 0) and (x < 1) and (y > 0) and (y < 1):\n", " rect.set_fc(np.random.random(3))\n", " fig.canvas.draw()\n", " \n", "fig.canvas.mpl_connect('button_press_event', on_click)" ], "language": "python", "metadata": {}, "outputs": [] }, { "cell_type": "heading", "level": 3, "metadata": {}, "source": [ "A Better Way: Pick Events" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Because clicking on polygons and other plot elements is such a common piece of many APIs,\n", "there is a cleaner and simpler way to do this: a pick event. A pick event is an event\n", "associated with any **artist** instance on the plot. We can re-create our random polygon\n", "example using pick events in this way:" ] }, { "cell_type": "code", "collapsed": false, "input": [ "fig, ax = plt.subplots()\n", "ax.set_xlim(-1, 2)\n", "ax.set_ylim(-1, 2)\n", "rect = plt.Rectangle((0, 0), 1, 1, fc=np.random.random(3), picker=True)\n", "ax.add_patch(rect)\n", "\n", "def on_pick(event):\n", " artist = event.artist\n", " artist.set_fc(np.random.random(3))\n", " fig.canvas.draw()\n", " \n", "fig.canvas.mpl_connect('pick_event', on_pick)" ], "language": "python", "metadata": {}, "outputs": [] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We can do a similar thing for a line rather than a polygon:" ] }, { "cell_type": "code", "collapsed": false, "input": [ "fig, ax = plt.subplots()\n", "x = np.linspace(0, 10, 1000)\n", "ax.plot(x, np.sin(x), c=np.random.random(3), lw=3, picker=5) # 5 points tolerance\n", "ax.plot(x, np.cos(x), c=np.random.random(3), lw=3, picker=5) # 5 points tolerance\n", "\n", "def on_pick(event):\n", " artist = event.artist\n", " artist.set_color(np.random.random(3))\n", " fig.canvas.draw()\n", " \n", "fig.canvas.mpl_connect('pick_event', on_pick)" ], "language": "python", "metadata": {}, "outputs": [] }, { "cell_type": "heading", "level": 3, "metadata": {}, "source": [ "Exercise" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Use the above framework to draw ``N`` circles, all of which can have\n", "their color changed by clicking on them:" ] }, { "cell_type": "code", "collapsed": false, "input": [], "language": "python", "metadata": {}, "outputs": [] }, { "cell_type": "heading", "level": 2, "metadata": {}, "source": [ "Going a bit crazy... " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now we'll do a bit of an exercise based on a [blog post](http://jakevdp.github.com/blog/2012/11/24/simple-3d-visualization-in-matplotlib/)\n", "that I did a while ago.\n", "\n", "I've written some code which will show a 3D cube on the matplotlib canvas,\n", "and your task is to add event hooks that allow it to be manipulated by\n", "clicking and dragging the mouse.\n", "\n", "There's a script which creates a cube object, which can be rotated, projected, and shown on a 2D axis. It can be used like this:" ] }, { "cell_type": "code", "collapsed": false, "input": [ "from tutorial_lib.simple_cube import Cube\n", "c = Cube()\n", "fig, ax = plt.subplots(figsize=(8, 8))\n", "c.add_to_ax(ax)\n", "c.rotate(c.x - c.y, -np.pi / 6)" ], "language": "python", "metadata": {}, "outputs": [] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The cube can be rotated by calling the `rotate` method with the following arguments:\n", "\n", "- ``axis``: a 3D vector specifying the direction of the axis of rotation\n", "- ``angle``: a rotation angle (in radians)\n", "\n", "We can call ``rotate()``, followed by the draw method to show the cube rotating:" ] }, { "cell_type": "code", "collapsed": false, "input": [ "for i in range(50):\n", " c.rotate([1, 0, 0], np.pi / 20)\n", " fig.canvas.draw()" ], "language": "python", "metadata": {}, "outputs": [] }, { "cell_type": "markdown", "metadata": {}, "source": [ "With this machinery in place, we can set up some key bindings to make this\n", "object interactive.\n", "\n", "Let's first bind the left and right arrows to y-axis rotation, and the up and\n", "down arrows to x-axis rotation:" ] }, { "cell_type": "code", "collapsed": false, "input": [ "def key_press(event):\n", " if event.key == 'left':\n", " c.rotate(c.y, np.pi / 12)\n", " elif event.key == 'right':\n", " c.rotate(c.y, -np.pi / 12)\n", " elif event.key == 'up':\n", " c.rotate(c.x, np.pi / 12)\n", " elif event.key == 'down':\n", " c.rotate(c.x, -np.pi / 12)\n", " fig.canvas.draw()\n", " \n", "fig.canvas.mpl_connect('key_press_event', key_press)" ], "language": "python", "metadata": {}, "outputs": [] }, { "cell_type": "heading", "level": 2, "metadata": {}, "source": [ "Challenge" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now we'll stop a bit for an exercise. This is your task:\n", "\n", "**Create a cube which can be manipulated by clicking and dragging**\n", "\n", "You'll likely need to set up three different callback functions:\n", "\n", "- `button_press_event` : when the mouse button is pressed, set an indicator variable to `True`, and store the x and y location.\n", "- `button_release_event` : when the mouse button is released, set an indicator variable to `False`\n", "- `motion_notify_event`: when the mouse moves, check if the button is pressed. If so, rotate the cube about the x or y axis (or both!), depending on the new x/y location (be sure to call ``draw()`` to re-draw the figure!)\n", "\n", "We'll take 10 minutes or so for you to try this." ] }, { "cell_type": "code", "collapsed": false, "input": [], "language": "python", "metadata": {}, "outputs": [] } ], "metadata": {} } ] }