Interfacce grafiche
Scarica zip esercizi
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
conconda
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.
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 è //
[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 quadratoNOTA 3: per la radice quadrata, usa
** (1 / 2)
. Qual’è il risultato di1 / 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])
[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:
Basic Plotting.ipynb (usa lo stile proprio di bqplot per creare i grafici)
Esempi di interazione esempi-bqplot/Interactions:
esempi-bqplot/Interactions/Interaction Layer.ipynb per vedere come permettere all’utente di selezionare un range in un grafico, o delle barre da un istogramma
Esempi avanzati in esempi-bqplot/Applications:
Esempio visualizzazione reti Mobile Patent Suits * Wealth of Nations - esempio evoluzione dati nel tempo
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:
se hai 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
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])