On utilise sqlalchemy pour accéder à la bdd. Celle-ci va utiliser la bibliothèque déjà installée psycopg2
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import Table, schema
from sqlalchemy.orm import sessionmaker
Les mêmes bib pour la visualisation et le traitement de données que précédemment. Notez DatetimeTickFormatter
afin de modifier l'aspect des dates, autrement elles sont rapidement illisibles (sans compter le format non ordonné des dates à l'américaine!).
import pandas as pd#traitement de données
import panel as pn
import holoviews as hv
import param
from bokeh.models.formatters import DatetimeTickFormatter
import hvplot.pandas
XFORMATTER = DatetimeTickFormatter()#formatter les dates
XFORMATTER.years = ['%Y-%m-%d']
XFORMATTER.months = ['%Y-%m-%d']
XFORMATTER.days = ['%d/%m']#en zoomant, on ne montre que le jour et mois, sans l'année
pn.extension()
import os
import datetime as dt
On commence par indiquer les paramétres d'authentification à la BDD, puis on génère le moteur
POSTGRES_USER = os.getenv('POSTGRES_USER')
POSTGRES_PASSWORD= os.getenv('POSTGRES_PASSWORD')
POSTGRES_ADDRESS= os.getenv('POSTGRES_ADDRESS')
engine = create_engine('postgresql://{0}:{1}@{2}/sensors'.format(POSTGRES_USER,
POSTGRES_PASSWORD,
POSTGRES_ADDRESS))
Ensuite on va utiliser un tour de magie de SQLAlchemy. Comme dit, tous mes capteurs sont dans le schéma "th" et ils ont tous le même format. Plutôt que d'aller les chercher un par un, on va simplement demander à SQL de récupérer tous les tableaux disponibles.
Session = sessionmaker(bind=engine)
session = Session()
Base = declarative_base()
Base.metadata.reflect(engine,schema='th')
print(Base.metadata.tables.keys())
dict_keys(['th.cage_escalier', 'th.salon', 'th.sdb', 'th.entree', 'th.chambre', 'th.paul', 'th.bureau', 'th.cuisine'])
Comme pour le dashboard live, on s'intéresse d'abord qu'au bureau
tablename = 'th.bureau'
table = Base.metadata.tables[tablename]
On filtre les dates entre aujourd'hui et il y a trois jours.
dates = (table.c.msg_ts <= dt.date.today()) & (table.c.msg_ts >= dt.date.today()-dt.timedelta(days=3))
query = session.query(table).filter(dates)
Et on injecte le tout directement dans l'instanciateur de pandas Dataframe
.
df = pd.DataFrame(query.all())
df.set_index('msg_ts').hvplot(y='humidity')
En formattant les dates, cette fois dans le sens français plutôt qu'américain.
df.set_index('msg_ts').hvplot(y='humidity',xformatter=XFORMATTER)
Ok tout fonctionne. Maintenant on va faire quelque chose de plus complexe. Le dashboard devra pouvoir:
Pour faire tout ça, on va utiliser une classe param
qui pourra être lu par Panel
.
En effet une classe param
permet de définir différents paramètres avec leur type, limites, et noms. Panel
saura lui-même générer les bons boutons et autres widgets afin de sélectionner ces paramètres. Il faudra composer des méthodes qui renvoient des objets interprétables par Panel
, comme un graphique Holoviews, et qui dépendent de certains paramètres.
Voir la doc: https://panel.holoviz.org/user_guide/Param.html
class Historic_Data_Panel(param.Parameterized):
"""
Classe avec deux méthodes qui renvoient des graphiques:
- show_plot pour comparer les pièces entre elle
- show_plot2 pour voir autant de graph qu'il n'y a de grandeurs sélectionnées dans une même pièce.
la dernière méthode, load_dataframes, sert à récupérer les données nécessaires à chaque visualisation.
"""
#toutes les valeurs paramètrables sont déclarés directement ici,
#une particularité de la classe parent param.Parameterized
#paramètres globaux: les dates butoires
upper_date = param.Date(default=dt.date.today(),bounds=(None,dt.date.today()))
lower_date = param.Date(default=dt.date.today()-dt.timedelta(days=3),bounds=(None,dt.date.today()))
t_list = list(Base.metadata.tables.keys())
#paramètre pour le premier plot: choix des pièces et de la valeur sélectionnée
table_selector = param.ListSelector(default=t_list[:1], objects=t_list)
y_selec = param.Selector(default='temperature',objects=['temperature','humidity','pressure',
'linkquality','battery','voltage'])
#param_tres pour le second plot: choix de la pièce et des valeurs sélectionnées
table_selector2 = param.Selector(default=t_list[0], objects=t_list)
y_selec2 = param.ListSelector(default=['temperature','humidity','pressure'],
objects=['temperature','humidity','pressure',
'linkquality','battery','voltage'])
@param.depends('upper_date','lower_date',watch=True)
def load_dataframes(self):
"""
Si on change les dates limites, il mettra à jour le contenu du dictionnaire self.dfs.
"""
self.dfs = {}#génère un dictionnaire vide
self.show_stuff.object = "{0} => {1}".format(self.lower_date,self.upper_date)
for tablename, table in Base.metadata.tables.items():#pour chaque table
#ci-dessous rajoute un filtre "where" dans la requête SQL
dates = ((table.c.msg_ts <= self.upper_date+dt.timedelta(days=1))
& (table.c.msg_ts >= self.lower_date))
query = session.query(table).filter(dates)
#puis on injecte la donnée avec la méthode all de la réponse
self.dfs[tablename] = pd.DataFrame(query.all())
@param.depends('table_selector','y_selec','upper_date','lower_date')
def show_plot(self):
"""
Si on change les dates limites, l'axe des y ou les capteurs sélectionnés,
renvoie un nouvel objet Holoviews de type Overlay.
"""
l=[]
grid_style = {"grid_line_color":"olive",
"minor_grid_line_color":None}
for tn in self.table_selector:
p = self.dfs[tn].hvplot.line(x='msg_ts',y=self.y_selec,rot=20,xformatter=XFORMATTER,
label=tn,responsive=True).opts(show_grid=True)
l.append(p)
o = hv.Overlay(l)
return pn.pane.HoloViews(o,sizing_mode="stretch_both",min_width=600,min_height=800)
@param.depends('table_selector2','y_selec2','upper_date','lower_date')
def show_plot2(self):
"""
Si on change les dates limites, l'axe des y ou le capteur sélectionné,
renvoie un nouvel objet Holoviews de type Layout en colonne.
"""
tn = self.table_selector2
l=[]
grid_style = {"grid_line_color":"olive",
"minor_grid_line_color":None}
def_opt = dict(gridstyle=grid_style,
show_grid=True)
for i, y in enumerate(self.y_selec2):
p = self.dfs[tn].hvplot.line(x='msg_ts',y=y,rot=20,xformatter=XFORMATTER,responsive=True,
min_height=150,min_width=300)
if i==0:
p.opts(xaxis='top',**def_opt)
else:
p.opts(xaxis=None, **def_opt)
l.append(p)
o = hv.Layout(l).cols(1)
return pn.pane.HoloViews(o,sizing_mode="stretch_both",min_width=600,min_height=800)
def __init__(self,**params):
super().__init__(**params)
#cet objet sert à afficher des messages si nécessaire
self.show_stuff = pn.pane.markup.Markdown(object='')
self.load_dataframes()
On instancie
mypanel = Historic_Data_Panel()
Maintenant il faut disposer les différents widgets et les méthodes qui renvoient des objects visibles. Pour ça on va combiner des objets Layout de Panel
: des colonnes et des lignes, mais aussi deux feuilles (ou tabs).
Si on ne veut que certains widgets à un endroit, on peut les nommer dans l'objet panel.Param
. Ci-dessous, les dates butoires:
date_selec = pn.Param(#pn.param est un type de layout qui sert à générer des widgets en fonction de paramètres.
mypanel.param,#l'origine des paramètres, ici de l'instance précédemment créée
widgets={#le type de widget qu'on veut peut-être sélectionné. On peut laisser aussi par défaut.
'upper_date':pn.widgets.DatePicker,
'lower_date':pn.widgets.DatePicker
},
parameters=[#les paramètres sélectionnés. Par défaut tous les paramètres sont sélectionnés
'upper_date',
'lower_date'],
default_layout=pn.Row,#en ligne plutôt qu'en colonne
height_policy='fit',#rendre adaptif.
width_policy='fit'#rendre adaptif.
)
date_selec
On va maintenant rajouter une des méthodes :
pn.Pane(mypanel.show_plot)
Notez que même si les widgets sont dans une autre cellule que le plot, le plot reste intéractif! Il n'y a qu'un seul paramètre "upper_date" pour l'instance "mypanel", peu importe combien de fois vous l'appelez. Faites le test en rappelant les widgets des dates:
date_selec
On va maintenant faire le dashboard en entier, avec les deux plots dans une tab différente et les widgets qui correspondent:
app = pn.Column(
pn.Param(mypanel.param,widgets={'upper_date':pn.widgets.DatePicker,
'lower_date':pn.widgets.DatePicker},
parameters=['upper_date','lower_date'],
default_layout=pn.Row,
height_policy='fit',
width_policy='fit'),
pn.Tabs(('Compare rooms',
pn.Row(
pn.Param(mypanel.param,
parameters=['table_selector','y_selec'],
height_policy='fit',
width_policy='fit'),
mypanel.show_plot,
sizing_mode='stretch_both'
)),
('Check one room',
pn.Row(
pn.Param(mypanel.param,
parameters=['table_selector2','y_selec2'],
height_policy='fit',
width_policy='fit'),
mypanel.show_plot2,
sizing_mode='stretch_both')
)
),
sizing_mode='stretch_both'
)
#l'appel à la méthode .servable() permet d'utiliser ce notebook comme code source de votre application.
app.servable()
Il y a certainement des points à améliorer, notamment le fait que changer une date induit toujours un appel vers la base de donnée, même si la donnée est déjà inscrite dans le jeu de données déjà chargé!
Mais notre dashboard est là!