16. Interfaces de usuario en Jupyter con ipywidgets#

Jupyter y IPython permiten no sólo visualizar datos sino también interactuar con ellos en tiempo real

Para lograr esto estudiaremos los componentes de la librería ipywidgets

Advertencia

Los callbacks de los widgets requieren conexión con un intérprete de IPython. Si estás viendo este cuadernillo desde phuijse.github.io/PythonBook entonces los widgets no se visualizarán correctamente

16.1. ¿Qué es un widget?#

Un widget es un elemento gráfico que permite interactuar con una aplicación

Un conjunto de widgets forman una interfaz de usuario gráfica (Graphical user interface o GUI)

La siguiente imagen muestra los widgets típicos que se encuentran en las aplicaciones de PC

https://upload.wikimedia.org/wikipedia/commons/d/d5/Widgets.png

La librería ipywidgets provee controles que permiten interactuar con funciones de Python

Entre los muchos widgets disponibles se encuentran:

  • Botones e selectores (check-box)

  • Listas desplegables (combo-box)

  • Campos de ingreso de texto (text-box)

  • Deslizadores (sliders)

Instalación

La forma más sencilla de instalar es usando conda

conda install ipywidgets

Si lo instalas de otra manera (pip o python setup.py) es necesario habilitar la extensión

pip install ipywidgets --user
jupyter nbextension enable --py widgetsnbextension

16.2. Creación semi-automática de widgets#

Una manera rápida de implementar widgets es usar el decorador interact

Al aplicar el decorador a una función convertiremos sus argumentos de entrada en widgets

Cada entrada genera y se enlaza a un widget según su tipo:

  • Un entero produce un widget IntSlider

  • Un flotante produce un widget FloatSlider

  • Un booleando produce un widget Checkbox

  • Una lista produce un widget Dropdown

  • Un string produce un widget Text

Los argumentos del decorador permiten entregar algunas opciones a los widgets

%matplotlib inline
import matplotlib.pyplot as plt
import numpy as np
import ipywidgets as widgets

# Se crea un widget por cada argumento de la función
@widgets.interact(x=(0, 10, 2), # El mínimo, máximo y paso para x
                  y=(-1., 1., 0.01) # El mínimo, máximo y paso para y
                  )

def print_cosas(x=0, 
                y=0., 
                z=True, 
                w=['foo','bar'], 
                v='foo'): 
    display(x, y, z, w, v)

16.3. Creación manual de widgets#

Para mayor control podemos crear los widgets de nuestra preferencia con sus respectivos constructores y luego enlazarlos a una función usando manualmente con la función interact

A continuación revisaremos algunos de ellos

16.3.1. Widgets numéricos#

Un desplazador o slider es un widget cuyos argumentos son

  • min: valor donde inicia

  • max: valor donde termina

  • step: el salto entre valores

  • value: valor inicial

El slider generará números entre esos rangos a medida que interactuamos con él

Se puede crear un desplazador que produce números enteros con IntSlider o flotantes con FloatSlider

Ejemplo

f = lambda a: display(a)
x = widgets.IntSlider(min=-100, max=100, step=5) # Esto crea el widget
widgets.interact(f, a=x) # Esto enlaza x con a para la función f
<function __main__.<lambda>(a)>

También se pueden generar tuplas de enteros y flotantes usando IntRangeSlider y FloatRangeSlider, respectivamente

Por otro lado los widgets te tipo text-box FloatText y IntText pueden usarse para pedir un número al usuario

f = lambda a: display(a)
x = widgets.FloatText(description="Por favor ingrese un número")
widgets.interact(f, a=x) # Esto enlaza el widget al primer argumento de f

En ambos casos podemos obtener el valor de los widgets usando

x.value

Podemos verificar todos los atributos del widget usando

x.keys

16.3.2. Widgets de texto#

Sirven para capturar y mostrar strings

  • Text: Da una linea en blanco para escribir, al apretar Enter se captura el string

  • Textarea: Da un bloque de texto para escribir, se comporta igual a Text

  • Label: Muestra un string

widgets.Text(value, # Texto por defecto
             placeholder, # Texto que aparece cuando está vacío
             description # Texto que aparece a la izquierda del cuadrado de texto
            )

Ejemplo

widgets.Text(placeholder='Escribe tu nombre acá', description='Nombre:')

16.3.3. Objeto Layout y atributo style#

El objeto Layout sirve para dar estilo a los widgets

El estilo se explicita usando CSS

Algunos argumentos útiles son

  • width: ancho del widget en pixeles

  • height: alto del widget en pixeles

  • margin: espacio entre el widget y otros componentes

Cada widget tiene un atributo llamado style para personalizarlo

La lista de atributos de estilo se puede acceder con

x = widgets.Button()
x.style.keys

Ejemplo

slider_layout = widgets.Layout(width='600px', height='20px', margin='10px')

def mi_funcion(x):
    display(x[0], x[1], (x[1] - x[0]))

range_slider = widgets.FloatRangeSlider(min=-100., max=100., step=0.01, 
                                        continuous_update=True, 
                                        description=r'Un argumento muy interesante:',
                                        layout=slider_layout)
range_slider.style.description_width = 'initial'
range_slider.style.handle_color = 'black'

widgets.interact(mi_funcion, x=range_slider);

16.3.4. Widget de tipo contenedor#

Son widgets que sirven para organizar otros widgets

Por ejemplo

  • HBox: Organiza los widgets en forma horizontal

  • VBox: Organiza los widgets en forma vertical

  • Tab: Crea pestañas con los widgets

Estos widgets reciben una lista de widgets y pueden anidarse

Ejemplo

firstname = widgets.Text(description='Nombre:')
lastname = widgets.Text(description='Apellido:')
widgets.VBox([firstname, lastname])

16.4. Callbacks y eventos#

16.4.1. Widget Button y atributo on_click#

widgets.Button(description, # Texto en el botón
               button_style='', # Estilo del botón: 'success', 'info', 'warning', 'danger' or ''
               icon='check', # Icono del botón
               ...
              )

Podemos darle funcionalidad al botón enlanzándolo con una función o callback.

Importante

La función debe tener un argumento. Este argumento corresponde al botón en sí

Especificamos la función utilizando el método on_click del botón

Ejemplo

insert_name = widgets.Text(description='Escribir tu nombre:')
insert_name.style.description_width = 'initial'
push_data = widgets.Button(description='Enviar')
display(widgets.HBox([insert_name, push_data]))

def boton_apretado(b):
    display(insert_name.value)
    
push_data.on_click(boton_apretado)

16.4.2. Atributo observe#

Podemos agregar acciones a otros widgets con el método observe

Importante

observe recibe una función de un argumento y el nombre del atributo que queremos “observar”

Cada vez que haya un cambio en el atributo se llamará la función que especificamos

El argumento de la función es un diccionario que tiene las siguientes llaves

  • owner : El widget que provocó el cambio

  • name : El nombre del atributo que está cambiando

  • old : El valor antiguo del atributo

  • new : El valor nuevo del atributo

Ejemplo

def handler(change):
    # change es un diccionario para el atributo value
    display(f"Hola {change['new']}, Chao {change['old']}")

sel_slider = widgets.Dropdown(description="Nombre:", 
                              options=["Pablo", "Eliana", "Hector"])

display(sel_slider)
sel_slider.observe(handler, names='value')

16.4.3. widget Output#

Este es un widget especial que puede usarse para redireccionar las salidas de otros widgets

  1. Primero creamos y mostrarmos Output

  2. Luego lo usamos de contexto para la salida de otro widget

Ejemplo

out = widgets.Output(layout={'border': '1px solid black'})
display(out)
def on_change(button):
    with out: 
        display(insert_name.value) # Esto se va escribir donde quiera que esté out

insert_name = widgets.Text(description='Escribir tu nombre')
insert_name.style.description_width = 'initial'
push_data = widgets.Button(description='Enviar')
display(widgets.HBox([insert_name, push_data]))
push_data.on_click(on_change)