Using interactive widgets – a piano in the notebook

Starting with IPython 2.0, we can put interactive widgets in notebooks to create rich GUI applications that interact with our Python kernel. IPython comes with a rich set of graphical controls such as buttons, sliders, and drop-down menus. We have full control of their placement and appearance. We can combine different widgets to form complex layouts. We can even create our own interactive widgets from scratch as we will see in the next recipe, Creating a custom Javascript widget in the notebook – a spreadsheet editor for pandas.

In this recipe, we will show many possibilities offered by the interactive widget API in IPython 2.0+. We will create a very basic piano in the notebook.

Getting ready

You need to download the Piano dataset from the book's website (http://ipython-books.github.io). This dataset contains synthetic sounds of piano notes obtained on archive.org (CC0 1.0 Universal license). It is available at https://archive.org/details/SynthesizedPianoNotes.

How to do it...

  1. Let's import a few modules as follows:
    In [1]: import numpy as np
            import os
            from IPython.display import (Audio, display,
                                         clear_output)
            from IPython.html import widgets
            from functools import partial
  2. To create a piano, we will draw one button per note. The corresponding note plays when the user clicks on the button. This is implemented by displaying an <audio> element as follows:
    In [2]: dir = 'data/synth'
    In [3]: # This is the list of notes.
            notes = 'C,C#,D,D#,E,F,F#,G,G#,A,A#,B,C'.split(',')
    In [4]: def play(note, octave=0):
                """This function displays an HTML Audio element
                that plays automatically when it appears."""
                f = os.path.join(dir, 
                     "piano_{i}.mp3".format(i=note+12*octave))
                clear_output()
                display(Audio(filename=f, autoplay=True))
  3. We are going to place all buttons within a container widget. In IPython 2.0, widgets can be organized hierarchically. One common use case is to organize several widgets in a given layout. Here, piano will contain 12 buttons for the 12 notes:
    In [5]: piano = widgets.ContainerWidget()

    Note

    The API for creating container widgets such as horizontal or vertical boxes has changed in IPython 3.0. Refer to IPython's documentation for more details.

  4. We create our first widget: a slider control that specifies the octave (0 or 1 here):
    In [6]: octave_slider = widgets.IntSliderWidget()
            octave_slider.max = 1
            octave_slider
  5. Now, we create the buttons. There are several steps. First, we instantiate a ButtonWidget object for each note. Then, we specify a callback() function that plays the corresponding note (given by an index) at a given octave (given by the current value of the octave slider). Finally, we set the CSS of each button, notably the white or black color.
    In [7]: buttons = []
            for i, note in enumerate(notes):
                button = widgets.ButtonWidget(description=note)
                
                def on_button_clicked(i, _):
                    play(i+1, octave_slider.value)
                    
                button.on_click(partial(on_button_clicked, i))
                
                button.set_css({
                          'width': '30px', 
                          'height': '60px',
                          'padding': '0',
                          'color': 
                              ('black', 'white')['#' in note],
                          'background':
                              ('white', 'black')['#' in note],
                               'border': '1px solid black',
                               'float': 'left'})
                
                buttons.append(button)
  6. Finally, we arrange all widgets within the containers. The piano container contains the buttons, and the main container (container) contains the slider and the piano. This can be implemented:
    In [8]: piano.children = buttons
    In [9]: container = widgets.ContainerWidget()
            container.children = [octave_slider,
                                  piano]
  7. By default, widgets are organized vertically within a container. Here, the octave slider will be above the piano. Within the piano, we want all notes to be arranged horizontally. We do this by replacing the default vbox CSS class by the hbox class. The following screenshot shows the piano in the IPython notebook:
    In [10]: display(container)
             piano.remove_class('vbox')
             piano.add_class('hbox')

How it works...

The IPython widgets are represented by rich objects that are shared between the Python kernel and the browser. A widget contains special attributes called trait attributes. For example, the value trait attribute of SliderWidget is dynamically and automatically linked to the value that is selected by the user in the notebook's slider.

This link is bidirectional. Changing this attribute in Python updates the slider in the notebook.

The placement of the widgets is controlled by container widgets and with CSS classes. You will find more information in the documentation.

This architecture enables the creation of rich graphical applications in the notebook that are backed by Python code.

There's more...

See also

  • The Creating a custom JavaScript widget in the notebook – a spreadsheet editor for pandas recipe