Interfacce grafiche

Introduzione

In questo tutoral affronteremo il tema delle interfacce grafiche (GUI: Graphical User Interfaces), usando:

  • i widget di Jupyter. Li abbiamo scelti perchè sono sorprendentemente flessibili e intuitivi. Come riferimento, seguiremo l’ottima User Guide ufficiale di Jupyter (in inglese)

  • bqplot per fare grafici interattivi

Questo tutorial non può certo essere un corso intero di Human Computer Interaction, ma dovrebbe permettervi di avere un’idea di come sviluppare delle interfacce rudimentali

Che fare

  • scompatta lo zip in una cartella, dovresti ottenere qualcosa del genere:

gui
    gui.ipynb
    gui-sol.ipynb
    my-webapp.ipynb
    gui-maps.ipynb
    jupman.py

ATTENZIONE: Per essere visualizzato correttamente, il file del notebook DEVE essere nella cartella szippata.

  • apri il Jupyter Notebook da quella cartella. Due cose dovrebbero aprirsi, prima una console e poi un browser. Il browser dovrebbe mostrare una lista di file: naviga la lista e apri il notebook interactive.ipynb

  • Prosegui leggendo il file degli esercizi, ogni tanto al suo interno troverai delle scritte ESERCIZIO, che ti chiederanno di scrivere dei comandi Python nelle celle successive.

Scorciatoie da tastiera:

  • Per eseguire il codice Python dentro una cella di Jupyter, premi Control+Invio

  • Per eseguire il codice Python dentro una cella di Jupyter E selezionare la cella seguente, premi Shift+Invio

  • Per eseguire il codice Python dentro una cella di Jupyter E creare una nuova cella subito dopo, premi Alt+Invio

  • Se per caso il Notebook sembra inchiodato, prova a selezionare Kernel -> Restart

Perchè fare interfacce grafiche ?

Per quanto le interfacce grafiche possano sembrare attrattive, prima di lanciarsi a crearle bisogna sempre farsi alcune domande fondamentali:

Qual’è lo scopo?

  • sperimentazione?

  • creare prototipi ?

  • prodotti per utenti finali?

Chi è l’utente?

  • Tu?

  • Altre persone ?

    • Che conoscenze hanno ?

  • dove viene usata l’interfaccia ?

    • a casa?

    • fuori casa?

      • c’è poco / tanto sole ?

Scelte di stile

  • interfaccia semplice o complessa ?

  • stile in-progress ( mockup) o prodotto finito ?

Scelte tecniche

Una volta identificati i requisiti, si possono fare le scelte tecniche, che riguardano linguaggi e architettura. Ci sono tantissime combinazioni possibili, ne menzioniamo solo alcune:

sito online, solo client

  • niente server

  • client browser con Javascript, HTML, CSS

  • client mobile app

    • Android (in Java) ?

    • iPhone (in Object-C) ?

sito online, client / server

  • server in Python (magari in Django)

  • client Javascript,HTML,CSS in browser

  • client in browser in Python transpilato a Javascript

  • client mobile app

offline, desktop (applicazioni native Python)

Risposte per oggi:

scopo: sperimentazione

utente: Tu !

architettura: client/server

  • server: Jupyter, con funzioni definite in Python dentro Jupyter

  • client: browser

    • browser gestito automaticamente dal server Jupyter

    • niente Javascript / HTML, pochissimo CSS

Morale: se un giorno dovrete fare applicazioni grafiche sul serio per utenti finali, prima leggetevi un buon libro di Human Computer Interaction e testate spesso le vostre interfacce con amici & parenti !

Installazione ipywidgets

Prima di avviare Jupyter, bisogna installare la libreria ipywidgets ed eventualmente abilitarla, a seconda del sistema operativo:

Anaconda:

  • conda install -c conda-forge ipywidgets

  • Installare ipywidgets con conda abiliterà automaticamente l’estensione per te

Linux/Mac:

  • installa ipywidgets (--user installa nella propria home):

python3 -m pip install --user ipywidgets
  • abilita l’estensione così:

jupyter nbextension enable  --py   widgetsnbextension

Adesso prova ad aprire Jupyter ed incollare il seguente codice in una cella, eseguendolo dovrebbe apparirti il widget dello slider sotto la cella:

[1]:
import ipywidgets as widgets
widgets.IntSlider()
[2]:
# copia incolla qua sotto:


Facciamo uno slider

Prima abbiamo dato l’istruzione a Jupyter di creare un widget slider, e Jupyter ci ha mostrato subito il risultato della creazione. Che succede se salviamo il risultato in una variabile ?

[3]:
w = widgets.IntSlider()

Vediamo che apparentemente non accade nulla. Cosa c’è adesso nella variabile w? Vediamolo con type:

[4]:
type(w)
[4]:
ipywidgets.widgets.widget_int.IntSlider

Vediamo che in w abbiamo un istanza di uno IntSlider. Ma come facciamo a dire a Python di mostrarlo? Possiamo usare la funzione display, importandola dal modulo IPython.display (Nota che il display dopo il punto è il nome del modulo, in questo caso particolare il modulo contiene anche una funzione che si chiama come il modulo):

[5]:
from IPython.display import display
display(w)

Proviamo a cambiare un po’ di proprietà dello slider:

[6]:
w.description = "ciao" # scritta a sinistra
[7]:
w.value
[7]:
0
[8]:
w.value = 30

Per una lista di variabili, usa keys:

[9]:
w.keys
[9]:
['_dom_classes',
 '_model_module',
 '_model_module_version',
 '_model_name',
 '_view_count',
 '_view_module',
 '_view_module_version',
 '_view_name',
 'continuous_update',
 'description',
 'description_tooltip',
 'disabled',
 'layout',
 'max',
 'min',
 'orientation',
 'readout',
 'readout_format',
 'step',
 'style',
 'value']

ESERCIZIO: prova a settare min, max, step. Prova a settare valori di min più grandi del valore che vedi correntemente, che succede ?

ESERCIZIO: Prova qualche altro widget dal sito di Jupyter

Model View Controller

Una cosa interessante è chiamare ripetutamente display:

[10]:
w = widgets.IntSlider()
display(w)
[11]:
display(w)

ESERCIZIO: prova a scorrere il secondo slider, e guarda cosa succede al primo

Ogni chiamata a display genera una vista (view) del widget, ma il modello sottostante (model) che contiene i dati con il numero della posizione rimane lo stesso. Ogni volta che clicchiamo su una vista e trasciniamo lo slider, il browser manda dei segnali al kernel Python per comunicargli che deve cambiare il valore nel modello dati. Il kernel controlla cosa succede (controller) e a sua volta può reagire ai segnali attuando altri comportamenti, come per esempio aggiornare un grafico nel browser.

WidgetModelView-7231

Interact

interact è un modo semplice per creare delle funzioni che reagiscono a cambiamenti di componenti grafici. Per esempio, se vogliamo creare uno slider alla cui posizione è associata una variabile k: ogni volta che muoviamo lo slider, ci piacerebbe stampare il doppio di k.

Per cominciare, possiamo definire una nostra funzione aggiorna, che prende in input il numero k, calcola il nuovo numero e stampa:

NOTA: aggiorna prende un parametro k che definiamo noi. Potremmo anche chiamarlo pippo.

[12]:
from ipywidgets import interact

def aggiorna(k = 3):
    print("il doppio è " + str(k*2))   # con str convertiamo il numero a stringa, altrimenti Python si offende

interact(aggiorna)
[12]:
<function __main__.aggiorna(k=3)>

Abbiamo creato una semplice funzione Python, niente di speciale fin qui. Proviamo a chiamarla noi:

[13]:
aggiorna(5)
il doppio è 10
[14]:
aggiorna(7)
il doppio è 14

Ora non ci resta che dire a Jupyther di creare uno slider e chiamare aggiorna ogni volta che lo slider viene spostato. Possiamo farlo con la funzione di Jupyter interact.

NOTA: chiamando la funzione di Jupyter interact, come parametro gli passiamo la funzione aggiorna, NON il risultato della funzione aggiorna ! Dato che nella dichiarazione di aggiorna abbiamo messo un parametro k inizializzato da un intero 3, Python capisce magicamente che siamo interessati a visualizzare un widget in grado di modificare valori interi, e in questo caso creerà un bello slider!

[15]:
interact(aggiorna)
[15]:
<function __main__.aggiorna(k=3)>

Abbiamo detto che interact è intelligente e crea il widget giusto in base al tipo del parametro iniziale. Proviamo con un boolean:

[16]:
from ipywidgets import interact


def aggiorna(k = True):
    if k:
        print("spuntata")
    else:
        print("non spuntata")

interact(aggiorna)
[16]:
<function __main__.aggiorna(k=True)>

Vediamo che ci viene creata una casella checkbox. Si possono anche passare più parametri per ottenere più widget:

[17]:
from ipywidgets import interact


def aggiorna(i=3, k = True):

    if i > 3:
        print('grande')
    elif i == 3:
        print('medio')
    else:
        print('piccolo')

    if k:
        print("spuntata")
    else:
        print("non spuntata")

interact(aggiorna)
[17]:
<function __main__.aggiorna(i=3, k=True)>

Possiamo anche crearci il widget direttamente associandolo alla stessa variabile che usiamo in aggiorna. Qua per esempio associamo noi uno slider con valore iniziale 10 alla variabile k. Notare che il 10 è definito durante la creazione dell’IntSlider e non in aggiorna:

[18]:
from ipywidgets import interact

def aggiorna(k):
    print('Il numero è ' + str(k))

interact(aggiorna, k=widgets.IntSlider(min=-10,max=30,step=1,value=10));

Riusare il widget con interactive

Se vogliamo accedere programmaticamente agli oggetti widget creati da interact, non dobbiamo usare interact ma interactive:

[19]:
from ipywidgets import interactive
from IPython.display import display

def aggiorna(k):
    print('Il numero è ' + str(k))

slider = interactive(aggiorna, k=widgets.IntSlider(min=-10,max=30,step=1,value=10));
display(slider)

Eventi

E se volessimo accedere ai valori di un widget con logiche più complesse, per esempio per comandarne un altro? In questi casi conviene gestire eventi con observe. Per cominciare, vediamo come funziona sul buon vecchio IntSlider.

  • Oltre a trascinare lo slider, prova anche a farlo saltare da un estremo all’altro, e osserva i valori stampati.

  • Per far sparire tutte le stampe, basta rieseguire la cella.

ATTENZIONE: attento ai bottoni !

.observe va bene per widget in genere, ma per i bottoni ad Agosto 2018 non sembra funzionare. Per quest’ultimi usa gli eventi click con il metodo .on_click.

[20]:
import ipywidgets as widgets
from ipywidgets import IntSlider
from IPython.display import display

slider1 = IntSlider()
display(slider1)

# notare che stavolta dichiariamo la variabile 'change', che NON è un solo valore come con interact
# ma un oggetto con diversi valori riguardanti il cambiamento generato dal click dell'utente,
# il più interessante dei quali è `new`. Prova a togliere i commenti
# da print(change) per vedere l'oggetto change completo
def aggiorna(change):
    print("Il nuovo valore è " + str(change.new))
    #print(type(change))
    print(change)

#NOTA: adesso passiamo anche il parametro names=['value'] per filtrare i tipi di change che riceviamo
slider1.observe(aggiorna, names=['value'])

ESERCIZIO: Perchè abbiamo aggiunto quel names=['value']? Ricopia il codice di sopra qua sotto togliendo , names=['value'] scoprirai che vengono stampate un sacco di cose in più, che nella maggior parte dei casi sono inutili.

[21]:
# scrivi qui


Controlliamo una label con uno slider

Proviamo adesso ad usare il widget dello slider per comandare dei widget di tipo Label:

[22]:
import ipywidgets as widgets
from ipywidgets import IntSlider, Label
from IPython.display import display

slider1 = IntSlider()
display(slider1)
label_new = Label("nuovo valore = ")
display(label_new)

# notare che stavolta dichiariamo la variabile 'change', che NON è un solo valore come con interact
# ma un oggetto con diversi valori riguardanti il cambiamento generato dal click dell'utente,
# il più interessante dei quali è `new`. Prova a togliere i commenti
# da print(change) per vedere l'oggetto change completo
def aggiorna(change):
    label_new.value = "nuovo valore = " + str(change.new) # convertiamo il numero in stringa

#NOTA: adesso passiamo anche il parametro names=['value'] per filtrare i tipi di change che riceviamo
slider1.observe(aggiorna, names=['value'])

ESERCIZIO: Riscrivi qua sotto l’esempio di sopra, aggiungiendo in più una etichetta che mostri anche il vecchio valore

Mostra soluzione
[23]:
# scrivi qui


Controlliamo uno slider con un’altro slider

Proviamo adesso ad usare il widget dello slider per comandare un altro slider: quando cambiamo i valori del primo slider, vogliamo che il secondo mostri lo stesso valore raddoppiato.

[24]:
import ipywidgets as widgets
from ipywidgets import IntSlider
from IPython.display import display

slider1 = widgets.IntSlider(max=100)  # specifichiamo il limite estremo
display(slider1)
slider2 = widgets.IntSlider(max=200)  # ricordiamoci di specificare il limite doppio del primo
display(slider2)


# notare che stavolta dichiariamo la variabile 'change', che NON è un solo valore come con interact
# ma un oggetto con diversi valori riguardanti il cambiamento generato dal click dell'utente,
# il più interessante dei quali è `new`. Prova a togliere i commenti
# da print(change) per vedere l'oggetto change completo
def aggiorna1(change):
    #print("Il nuovo valore è " + str(change.new))
    #print(type(change))
    slider2.value = change.new * 2

#NOTA: adesso passiamo anche il parametro names=['value'] per filtrare i tipi di change che riceviamo
slider1.observe(aggiorna1, names=['value'])

ESERCIZIO: Cosa succede se muoviamo il secondo slider? Il primo si aggiorna? Scrivi qua sotto una versione modificata del codice sopra, in cui quando si muove il secondo slider al primo viene fatto mostrare un valore che è la metà del secondo.

SUGGERIMENTO: crea una seconda funzione aggiorna2 che aggiorna il primo slider, e collegala allo slider2

NOTA: l’operatore di divisione intera è //

Mostra soluzione
[25]:
# scrivi qui


ESERCIZIO: Dopo aver svolto l’esercizio precedente, prova a farne un’altro dove gli slider sono FloatSlider e il secondo slider rappresenta il quadrato del primo slider.

  • NOTA 1: Ricordati di importare il FloatSlider

  • NOTA 2: l’operatore di esponenziazione in Python è **: 5 ** 2 è cinque al quadrato

  • NOTA 3: per la radice quadrata, usa ** (1 / 2). Qual’è il risultato di 1 / 2?

DOMANDA: se hai svolto l’esercizio precedente, probabilmente avrai notato che gli slider sembrano ‘ballare’ un po’ più del caso intero. Hai idea del perchè ?

Mostra soluzione
[26]:

import ipywidgets as widgets
from ipywidgets import FloatSlider, Label
from IPython.display import display


# scrivi qui


Eventi di widget a selezione multipla

Vediamo adesso le change di un widget di selezione multipla:

[27]:
sm = widgets.SelectMultiple(
    options=['Apples', 'Oranges', 'Pears'],
    value=['Oranges'],
    #rows=10,
    description='Fruits',
    disabled=False
)

def aggiorna(change):
    #print("Il nuovo valore è " + str(change['new']))
    #print(type(change))
    print(change)

#NOTA: adesso passiamo anche il parametro names=['value'] per filtrare i tipi di change che riceviamo
sm.observe(aggiorna, names=['value'])

display(sm)

Eventi click

Attento ai bottoni ! .observe va bene per widget in genere, ma per i bottoni ad Agosto 2018 non sembra funzionare. Per quest’ultimi usa gli eventi click con il metodo .on_click così:

[28]:

import ipywidgets as widgets
from IPython.display import display
from ipywidgets import Button

def cliccato(b):
    # nota che on_click passa il widget cliccato

    print("Questo bottone è stato cliccato:\n%s" % b)

bottone = widgets.Button(
        description='Stampa',
        disabled=False,
        button_style='',
        tooltip='stampa qualcosa',
        icon='check'
    )

bottone.on_click(cliccato)
display(bottone)


Layout e stili

Quando si vuole posizionare widget, si parla generalmente di layout. Ci sono tanti modi di posizionarli, vediamo i principali:

Layout: HBox

Possiamo usare HBox per mettere i widget uno dopo l’altro in orizzontale:

[29]:
from ipywidgets import Button, IntSlider, HBox, VBox, Label

HBox([IntSlider(), Button(description='hello')])

Alcuni widget hanno parametri appositi per associare testo al widget, ma talvolta il testo viene accorciato senza il nostro consenso. Per ovviare a ciò possiamo usare il widget Label, che consente di forzare la visualizzazione di testo lungo, in combinazione con HBox:

[30]:
HBox([widgets.Label('A too long description:'),  widgets.IntSlider()])

Layout: VBox and HBox

Ovviamente c’è anche il layout verticale VBox. Per ottenere configurazioni a griglia, è possibile usare un misto di HBox e VBox :

[31]:

from ipywidgets import Button, HBox, VBox

left_box = VBox([Button(description='alto a sinistra'), Button(description='basso a sinistra')])
right_box = VBox([Button(description='alto a destra'), Button(description='basso a destra')])
HBox([left_box, right_box])

Flexbox

Se hai esigenze di layout complesse, raccomandiamo di cuore i FlexBox, che sono praticamente l’unico sistema di layout decente del modello CSS (Cascading Style Sheet, di cui avete provato il linguaggio di query nel capitolo sull’estrazione dati da HTML). Per fortuna Jupyter li supporta nativamente e li potete programmare direttamente in Python. Per una descrizione completa rimandiamo al sito di Jupyter Widgets

Stile

E’ possibile applicare vari stili ai bottoni, di nuovo facendo riferimento alle proprietà CSS che sono supportate da Jupyter. Esempio

[32]:
from ipywidgets import Button, Layout

b = Button(description='Bottone con stile applicato ',
           layout=Layout(width='50%', height='80px'))
b

Grafici interattivi con bqplot

Bqplot è una libreria molto potente per realizzare grafici in Jupyter. In particolare bqplot :

  • riprende i comandi già visti per matplot (stile matlab), che quindi possono riusati pari pari

  • si integra bene con ipywidgets

  • permette di esportare i grafici in codice interattivo HTML, facilmente inseribile in siti web, blog, etc..

Installazione bqplot

Anaconda:

Apri Anaconda Prompt (per istruzioni su come trovarlo o se non hai idea di cosa sia, prima di proseguire leggi sezione interprete Python nell’introduzione) ed esegui:

conda install -c conda-forge bqplot

Installare bqplot con conda abiliterà automaticamente l’estensione per te in Jupyter

Linux/Mac:

  • installa ipywidgets (--user installa nella propria home):

python3 -m pip install --user bqplot
  • abilita l’estensione così:

jupyter nbextension enable --py bqplot

bqplot - il primo grafico

Adesso prova ad aprire Jupyter ed incollare il seguente codice in una cella, eseguendolo dovrebbe apparirti un grafico

Abbiamo parlato di grafici modificabili interattivamente, proviamo a crearne uno. Intanto creiamo un semplice grafico con bqplot, riprendendo istruzioni dal tutorial sulla Visulazzazione

ATTENZIONE al plt!

Il pyplot che vedete qui sotto, che viene importato con il nome di plt proviene dalla libreria di bqplot, non è lo stesso pyplot di matplotlib !!

Gli autori di bqplot hanno adottato lo stesso nome e convenzioni per permettervi di riusare facilmente esempi che già conoscete di matplotlib, ma NON E’ AFFATTO DETTO CHE TUTTI GLI ESEMPI DI MATPLOTLIB FUNZIONINO ANCHE CON BQPLOT !!

[33]:

# !!!!  IMPORTANTE !!!!
# Il 'pyplot' che vedete qui sotto, che viene importato con il nome di 'plt'
# proviene dalla libreria di bqplot, quindi NON E' lo stesso pyplot di matplotlib !!
# Gli autori di bqplot hanno adottato lo stesso nome e convenzioni per permettervi
# di riusare facilmente esempi che già conoscete di matplotlib

from bqplot import pyplot as plt

plt.figure(title='Grafico in bqplot')
x = [1,2,3,4,5]
y = [2,4,8,16,32]
plt.plot(x, y, 'bo') # b=blue o=punti (sostuendo 'o' con 'l' farà delle linee)
plt.show()

bqplot - variare parametri

Visto un esempio che già conosciamo bene, proviamo a plottare una funzione un po’ più complessa, come per esempio un coseno:

[34]:
# !!!!  IMPORTANTE !!!!
# Il 'pyplot' che vedete qui sotto, che viene importato con il nome di 'plt'
# proviene dalla libreria di bqplot, NON E' lo stesso pyplot di matplotlib !!
# Gli autori di bqplot hanno adottato lo stesso nome e convenzioni per permettervi
# di riusare facilmente esempi che già conoscete di matplotlib

from bqplot import pyplot as plt
import numpy as np


x = np.linspace(0, 2 * np.pi, 50)  # mette nel vettore x 50 punti tra 0 e 2 pi greco (inclusi)
fig = plt.figure()                 # genera la figure

# plottiamo un coseno
# plot ritorna una lista di linee Line2D, ma all'interno in questo caso ne contiene una sola che estraiamo:
lines = plt.plot(x, np.cos(x))
plt.title('Grafico in bqplot')
plt.show()

Abbiamo creato il grafico, usando solo bqplot (non matplotlib, vedi commenti nel codice!). Per poter variare il grafico, dovremo indicare a bqplot dei nuovi valori per le y. Come fare? Se avete notato, prima abbiamo salvato il risultato di plot nella variabile lines. Ma cos’è esattamente? Che campi contiene? Scopriamolo:

[35]:
type(lines)
[35]:
bqplot.marks.Lines
[36]:
lines
[36]:
Lines(colors=['#1f77b4', '#ff7f0e', '#2ca02c', '#d62728', '#9467bd', '#8c564b', '#e377c2', '#7f7f7f', '#bcbd22', '#17becf'], interactions={'hover': 'tooltip'}, scales={'x': LinearScale(), 'y': LinearScale()}, scales_metadata={'x': {'orientation': 'horizontal', 'dimension': 'x'}, 'y': {'orientation': 'vertical', 'dimension': 'y'}, 'color': {'dimension': 'color'}}, tooltip_style={'opacity': 0.9}, x=array([0.        , 0.12822827, 0.25645654, 0.38468481, 0.51291309,
       0.64114136, 0.76936963, 0.8975979 , 1.02582617, 1.15405444,
       1.28228272, 1.41051099, 1.53873926, 1.66696753, 1.7951958 ,
       1.92342407, 2.05165235, 2.17988062, 2.30810889, 2.43633716,
       2.56456543, 2.6927937 , 2.82102197, 2.94925025, 3.07747852,
       3.20570679, 3.33393506, 3.46216333, 3.5903916 , 3.71861988,
       3.84684815, 3.97507642, 4.10330469, 4.23153296, 4.35976123,
       4.48798951, 4.61621778, 4.74444605, 4.87267432, 5.00090259,
       5.12913086, 5.25735913, 5.38558741, 5.51381568, 5.64204395,
       5.77027222, 5.89850049, 6.02672876, 6.15495704, 6.28318531]), y=array([ 1.        ,  0.99179001,  0.96729486,  0.92691676,  0.8713187 ,
        0.80141362,  0.71834935,  0.6234898 ,  0.51839257,  0.40478334,
        0.28452759,  0.1595999 ,  0.03205158, -0.09602303, -0.22252093,
       -0.34536505, -0.46253829, -0.57211666, -0.67230089, -0.76144596,
       -0.8380881 , -0.90096887, -0.94905575, -0.98155916, -0.99794539,
       -0.99794539, -0.98155916, -0.94905575, -0.90096887, -0.8380881 ,
       -0.76144596, -0.67230089, -0.57211666, -0.46253829, -0.34536505,
       -0.22252093, -0.09602303,  0.03205158,  0.1595999 ,  0.28452759,
        0.40478334,  0.51839257,  0.6234898 ,  0.71834935,  0.80141362,
        0.8713187 ,  0.92691676,  0.96729486,  0.99179001,  1.        ]))

Il campo che ci interessa è y:

y=array([ 1.        ,  0.99179001,  0.96729486,  0.92691676,  0.8713187 ,

Proviamo a cambiarlo, variando per esempio la frequenza del coseno moltiplicando x per 5:

lines.y = np.cos(5 * x)

ESERCIZIO: Prova a incollare il codice di sopra qui sotto, eseguilo e poi guarda se il grafico di prima è cambiato. Prova anche a variare il numero davanti a x

[37]:
# copia qui


Adesso ci piacerebbe aggiungere uno slider per cambiare interattivamente la frequenza dell’onda, indicheremo tale frequenza con la variabile k. Per fare ciò, possiamo definire una nostra funzione aggiorna, che prende in input la frequenza k, e ricalcola le lines.y

NOTA: aggiorna prende un parametro k che definiamo noi. Potremmo anche chiamarlo pippo.

[38]:
def aggiorna(k = 1.0):
    lines.y = np.cos(k * x)

Ora non ci resta che dire a Jupyter di creare uno slider e chiamare aggiorna ogni volta che lo slider viene spostato. Possiamo farlo con la funzione di Jupyter interact.

NOTA: chiamando la funzione di Jupyter interact, come parametro gli passiamo la funzione aggiorna, NON il risultato della funzione aggiorna ! Dato che nella dichiarazione di aggiorna abbiamo messo un parametro k inizializzato da un float 1.0, Python capisce magicamente che siamo interessati a visualizzare un widget in grado di modificare valori float, e in questo caso creerà un bello slider!

[39]:
from ipywidgets import interact

interact(aggiorna);

Ricapitolando, ecco tutto il codice completo:

[40]:
# !!!!  IMPORTANTE !!!!
# Il 'pyplot' che vedete qui sotto, che viene importato con il nome di 'plt'
# proviene dalla libreria di bqplot, NON E' lo stesso pyplot di matplotlib !!
# Gli autori di bqplot hanno adottato lo stesso nome e convenzioni per permettervi
# di riusare facilmente esempi che già conoscete di matplotlib

from bqplot import pyplot as plt
import numpy as np
from ipywidgets import interactive

x = np.linspace(0, 2 * np.pi, 50)  # mette nel vettore x 50 punti tra 0 e 2 pi greco (inclusi)
fig = plt.figure()                 # genera la figure

# plottiamo un coseno
# plot ritorna una lista di linee Line2D, ma all'interno in questo caso ne contiene una sola che estraiamo:
lines = plt.plot(x, np.cos(x))
plt.title('Grafico in bqplot')
plt.show()

def aggiorna(k = 1.0):
    lines.y = np.cos(k * x)
slider_frequenza = interactive(aggiorna);

display(slider_frequenza)

bqplot - layout

Nel paragrafo precedente, abbiamo visto un primo esempio funzionante che ci permette di variare un parametri e mostrare gli aggiornamenti nel grafico. Come prossimo passo potremmo voler posizionare lo slider a sinistra del plot. Potremmo farlo usando i layout.

Il bello di Bqplot è che è ben integrato con gli ipywidgets. Per esempio, possiamo provare ad inserire il grafico del coseno di bqplot dentro un HBox, mettendo a sinistra lo slider usato in precedenza per variare la frequenza:

[41]:
from bqplot import pyplot as plt
import numpy as np
from ipywidgets import interactive
from ipywidgets import HBox, Box

x = np.linspace(0, 2 * np.pi, 50)  # mette nel vettore x 50 punti tra 0 e 2 pi greco (inclusi)
fig = plt.figure()                 # genera la figure

# plottiamo un coseno
# plot ritorna una lista di linee Line2D, ma all'interno in questo caso ne contiene una sola che estraiamo:
lines = plt.plot(x, np.cos(x))
plt.title('HBox con slider e bqplot')

def aggiorna(k = 1.0):
    lines.y = np.cos(k * x)

# otteniamo la variabile widget con interactive (che a differenza di 'interact' ritorna un widget !)
slider_frequenza = interactive(aggiorna);

# Creiamo l'HBox passando in una lista prima la variabile dello slider, e poi la `fig` di bqplot
HBox([slider_frequenza, fig])

ESERCIZIO: Nell’esempio qua sopra, lo slider appare forse un po’ troppo in alto. Se guardi la documentazione degli HBox vedrai che gli HBox vengono definiti come dei Box aventi proprietà prese dal modello FlexiBox:

def HBox(*pargs, **kwargs):
    """Displays multiple widgets horizontally using the flexible box model."""
    box = Box(*pargs, **kwargs)
    box.layout.display = 'flex'
    box.layout.align_items = 'stretch'
    return box

Cercando un po’ nella documentazione sull’allineamento oggetti in FlexiBox, riuesciresti a capire quale parametro Flexbox modificare affinchè lo slider risulti a metà altezza rispetto al grafico (nota che ci sono sia align-items e align-content)? Fai dei tentativi modificando la funzione MiaHBox qua sotto:

[42]:
# ESERCIZIO: guarda sotto la funzione 'MiaHBox'

from bqplot import pyplot as plt
import numpy as np
from ipywidgets import interactive
from ipywidgets import HBox, Box

x = np.linspace(0, 2 * np.pi, 50)  # mette nel vettore x 50 punti tra 0 e 2 pi greco (inclusi)
fig = plt.figure()                 # genera la figure

# plottiamo un coseno
# plot ritorna una lista di linee Line2D, ma all'interno in questo caso ne contiene una sola che estraiamo:
lines = plt.plot(x, np.cos(x))
plt.title('HBox con slider e bqplot')

def aggiorna(k = 1.0):
    lines.y = np.cos(k * x)

# otteniamo la variabile widget con interactive (che a differenza di 'interact' ritorna un widget !)
slider_frequenza = interactive(aggiorna);

# Ripreso da documentazione di HBox

# ESERCIZIO: MODIFICA QUESTA FUNZIONE

def MiaHBox(*pargs, **kwargs):
    """Displays multiple widgets horizontally using the flexible box model."""
    box = Box(*pargs, **kwargs)
    box.layout.display = 'flex'
    box.layout.align_items = 'stretch'
    return box

MiaHBox([slider_frequenza, fig])
Mostra soluzione
[43]:

bqplot - esempi avanzati

Nella cartella esempi-bqplot che sta nella zip degli esercizi abbiamo inserito diversi esempi di bqplot, ti invitiamo a guardarli. Tanto per dare un’idea, ne mettiamo qui alcuni di rilevanti (scorri in fondo alle relative pagine per vedere i grafici visualizzati - a volte potrebbe anche essere necessario rieseguire il tutto con Kernel->Restart and Run All):

Esempi base in esempi-bqplot/Basic Plotting:

Pyplot.ipynb

Esempi di interazione esempi-bqplot/Interactions:

Esempi avanzati in esempi-bqplot/Applications:

HTML

Il codice HTML è il codice con cui sono scritte tutte le pagine web, e gli ipywidgets permettono di creare un widget a partire da codice HTML. Qui per esempio lo usiamo per creare un titolo, ma in genere per fare interfacce di moderata complessità non è indispensabile conoscerlo. Se vuoi saperne di più, prova a seguire il tutorial web 1 di coderdojotrento

[44]:
from ipywidgets import HTML
import ipywidgets as widgets

HTML('<h1 style="color:orange">Il mio titolo</h1> <br/>')

Mappe

E’ possibile controllare da Python delle mappe geografiche visualizzate in Jupyter con la libreria ipyleaflet e OpenStreetMap, la mappa libera del mondo realizzata da volontari.

Vedi tutorial nel foglio gui-maps.ipynb

Chatbot

Mostriamo un esempio di come costruire una semplice interfaccia stile chatbot con ipywidgets, che continua a proporre widget di input all’utente e relativo output.

Per i grafici, importeremo bqplot plt invece invece di matplotlib, perchè è più pensato per interagire con ipywidgets !

NOTA: Una volta effettuata una selezione, apparirà sotto un nuovo input, ma l’input precedente sarà ancora modificabile. Come esercizio, potresti provare a rendere l’input precedente non più modificabile una volta che è stato scelto un valore.

[45]:
# import matplotlib.pyplot as plt  # usiamo bqplot
from ipywidgets import IntSlider, Label, VBox, HTML
import ipywidgets as widgets

# NOTA: importiamo bqplot plt invece di matplotlib, perchè è più pensato per
#       interagire con ipywidgets !

from bqplot import pyplot as plt


def aggiorna(change):
    if change.new !="Select":
        #print(change)
        #print(change.new)
        scelta=change.new

        labels = ['oggi', 'domani', 'dopodomani']
        ys = [2,5,1]

        fig = plt.figure()

        xticks = labels

        p1 = plt.bar(xticks, ys, width=0.3 )

        #p2 = plt.bar(xticks,y, color=['b','g','r'], width=0.3, align="center", bottom=p1)

        plt.title(scelta)

        vbox.children = vbox.children + (fig,crea_widget())


def crea_widget():
    dropdown = widgets.Dropdown(
        options=['Select', 'Trento', 'Rovereto', 'Bolzano'],
        value='Select',
        description='Città:',
        disabled=False,
    )

    dropdown.observe(aggiorna,  names=['value'])
    return dropdown

vbox = VBox([HTML("<h1>Chatbot Quanto pioverà?</h1>"),crea_widget()])


display(vbox)


Webapp

Finora abbiamo visualizzato i widget dentro Jupyter, ma ti starai chiedendo se puoi mostrarli come fossero in un vero e proprio sito.

Per ottenere un risultato simile ad una webapp, possiamo usare Voilà

Installazione voila:

ATTENZIONE: la a finale di voila non è accentata

conda install -c conda-forge voila
  • se hai Linux/Mac, scrivi:

python3 -m pip install --user voila

Esempio my-webapp

Una webapp può semplicemente essere un singolo foglio Jupyter, anche quando prevediamo più pagine per il sito. In genere, si può fare un widget contenitore che chiameremo mia_app e quando si vuole simulare il combiamento di una pagina, si sostituisce un pannello all’interno di mia_app.

Abbiamo creato un esempio di webapp nel file mia-webapp.ipynb che è fornito nella stessa cartella di questi esercizi.

ATTENZIONE: mia-webapp.ipynb per funzionare ha bisogno che installi anche bqplot

Se guardi nel file my-webapp.ipynb](my-webapp.ipynb) troverai una variabile my_app che è un widget VBox:

mia_app = VBox( children=[titolo,    # supponiamo che il titolo sia sempre visibile in tutto il sito
                          pagina1,   # al momento la prima 'pagina' è il widget tab
                          credits])  # supponiamo che il titolo sia sempre visibile in tutto il sito

Sono definite anche due variabili pagina1 e pagina2 che sono widget che rappresentano i pannelli centrali. Quando vuoi sostituire un pannello, puoi chiamare la funzione cambia_pagina, passando la pagina desiderata:

cambia_pagina(pagina2)

Eseguiamo la webapp con voila

Voila dovrebbe permetterti di vedere la webapp senza gli ingombranti menu di Jupyter. Vediamo quindi cosa succede quando provi ad eseguire il notebook my-webapp.ipynb come se effettivamente fosse una webapp.

Una volta installato voila, da dentro il prompt dei comandi, (che è una finestra nera dove puoi immettere comandi testuali per il sistema operativo), raggiungi la cartella dove è contenuto questo foglio di esercizi, cioè gui

(per vedere in che cartella sei, scrivi dir, per entrare in una cartella che si chiama CARTELLA, scrivi cd CARTELLA)

Una volta nella cartella interactive, scrivi

voila my-webapp.ipynb --VoilaConfiguration.file_whitelist="['.*']"

il --VoilaConfiguration.file_whitelist="['.*']" serve a dire a voila che vogliamo che tutti i file presenti nella cartella servita siano accessibili, altrimenti per es. le immagini non saranno trovate. Questo va bene quando sviluppi, ma se ha in intenzione di pubblicare realmente il sito leggi la documentazione

Nel prompt, dovrebbero apparire le scritte simili, e si dovrebbe anche aprire un browser internet con visualizzato il notebook come webapp:

[Voila] Using /tmp to store connection files
[Voila] Storing connection files in /tmp/voila__4pcw5dx.
[Voila] Serving static files from /home/da/Da/bin/anaconda3/lib/python3.7/site-packages/voila/static.
[Voila] Voila is running at:
http://localhost:8866/
[Voila] Kernel started: 40fff204-d83a-4062-892c-daffaba3f9bd

Per spegnere il server, premi Control-C. Lo spegne in malo modo ma va bene lo stesso:

^C[Voila] Stopping...
[Voila] Kernel shutdown: 40fff204-d83a-4062-892c-daffaba3f9bd

Layout per webapp

Per disporre i widget abbiamo fornito alcuni esempi in my-webapp.ipynb, altri più avanzati li puoi trovare in questo tutorial (in inglese) e nella documentazione di Jupyter

Collegare i widget

Guardando la sezione precedente sugli eventi, avrai notato che a volte modificando uno slider, se un’altro slider è collegato a volte potrebbe ‘ballare’ un po’. Magari non è piacevolissimo, ma per i vostri progetti dovrebbero essere sufficienti. Per completezza, menzioniamo comunque che per evitare i tremolii si possono usare i cosiddetti traitlets . Qua riportiamo solo un esempio veloce, per approfondire vedere la documentazione:

[46]:
import traitlets
slider_intero_del = widgets.IntSlider(description="slider delayed", continuous_update=False)
testo_intero_del = widgets.IntText(description="testo delayed", continuous_update=False)

traitlets.link((slider_intero_del, 'value'), (testo_intero_del, 'value'))
widgets.VBox([slider_intero_del, testo_intero_del])
[47]:
import traitlets
slider_intero_con = widgets.IntSlider(description="slider con", continuous_update=True)
testo_intero_con = widgets.IntText(description="testo con", continuous_update=True)

traitlets.link((slider_intero_con, 'value'), (testo_intero_con, 'value'))
widgets.VBox([slider_intero_con, testo_intero_con])