Le blog tech de Nicolas Steinmetz (Time Series, IoT, Web, Ops, Data)
La société SenX a proposé un code contest suite à la publication de son article sur les formes géospatiales. L’objet du concours porte sur le trajet d’un véhicule aux USA et il consiste à déterminer :
Maintenant que le gagnant a été annoncé (TL;DR: moi 😎🎉) et en attendant le corrigé officiel, voici ma proposition de solution.
Les données de départ sont :
@senx/dataset/route66_vehicle_gts
: le trajet réalisé par le véhicule@senx/dataset/route66_geoshape
: la route 66// Define points from the car journey on the US66 road
[
// Here is the gts of the car datalogger
@senx/dataset/route66_vehicle_gts
// Here is the route 66 geoshape (+/- 20meters)
@senx/dataset/route66_geoshape
mapper.geo.within 0 0 0
] MAP
"onTheRoad" STORE
$onTheRoad
{
'timesplit' 60 s
}
MOTIONSPLIT
0 GET
'sectionOnTheRoad' STORE
// Compute distance for each GTS and output it as a single point
[ $sectionOnTheRoad mapper.hdist MAXLONG MAXLONG 1 ] MAP
// Sum all GTS
0 SWAP <% VALUES 0 GET + %> FOREACH
// Convert to km
1000 /
// Enjoy !
Explications :
mapper.geo.within
(doc). Ce mapper compare deux zones géographiques et ne retient que les poits qui sont dans la zone voulue. Ici, je prends donc tous les points du trajet et les compare avec ceux de la route 66. Seuls les points sur la route 66 sont conservés. Le résultat est une aggrégation de points que l’on stocke dans la variable onTheRoad
.MOTIONSPLIT
(doc) pour calculer la distance entre deux points. Obtenant une liste de 1 élément contenant une liste, j’ai rajouté le 0 GET
pour supprimer la liste parente. On obtient alors une liste de 8 séries temporelles (GTS) correspondant à chaque tronçon sur la route. On stocke cela dans la variable sectionOnTheRoad
.mapper.hdist
(doc) permet de calculer la distance totale sur une fenêtre glissante de points. L’utilisation de MAXLONG
permet d’avoir une valeur suffisamment grande pour notre cas d’espèce pour prendre l’ensemble des données de chacune des 8 listes - il n’est pas nécessaire de connaitre la taille exacte de la liste pour travailler dessus et cela ne crée pas d’erreur non plus ; ça peut déstabiliser !. Le 1
permet de n’avoir qu’une valeur en sortie. On a donc en sortie la distance de chacune des 8 sections.0
(pour initialiser l’opération d’addition) et ajouter la première valeur de la liste et ainsi de suite. Une fois qu’on a la somme, on divise par 1000 pour avoir des kilomètres79.82147744769853
Pour comprendre la partie 2, on peut réécrire la chose de la façon suivante :
[ $sectionOnTheRoad mapper.hdist MAXLONG MAXLONG 1 ] MAP
'totalDistancePerSection' STORE
0 $totalDistancePerSection <% VALUES 0 GET + %> FOREACH
Non, toujours pas ? Vous me rassurez, j’ai du creuser plus loin aussi.
Commençons par :
$totalDistancePerSection <% VALUES 0 GET %> FOREACH
VALUES
(doc) consomme une série temporelle et en sort les valeurs sous la forme d’une liste. Nous avons une liste initiale de 8 séries que nous avons ramené à 8 points. Avec FOREACH
(doc), on applique donc la fonction VALUES
sur chaque série contenant un seul point. Plutôt que d’avoir en sortie des listes à un seul point, le 0 GET
permet d’avoir directement la valeur.
Pour faire une addition, en WarpScript, c’est :
1 1 +
ou :
1
1
+
Par celà, j’entends que pour appliquer +
, il faut que les deux éléments soient définis dans la pile.
Notre boucle FOREACH
emet dans la pile chaque valeur qu’il faut ajouter à la précédente. On peut donc rajouter le +
dans la boucle FOREACH
:
$totalDistancePerSection <% VALUES 0 GET + %> FOREACH
Mais si je cherche à exécuter cela, cela ne fonctionne pas - cela reviendrait à faire:
valeur1IssueDuForeach +
valeur2IssueDuForeach +
valeur3IssueDuForeach +
valeur4IssueDuForeach +
...
Si on part de la fin, la valeur 4 va pouvoir être additionnée à la valeur 3 car celle-ci existe dans la pile. MAIS la valeur 1 n’est additionnée à rien à ce stade et l’opération est invalide. D’où la nécessité de rajouter le 0
pour pouvoir avoir deux éléments pour notre première addition.
Ce qui nous donne bien :
0 $totalDistancePerSection <% VALUES 0 GET + %> FOREACH
Maintenant que la brume s’est éclaircie et que le 🤯 est passé à 😎 pour cette syntaxe de fin, je vous propose de nous retrouver dans un prochain billet pour la suite de ma solution au concours.
compose
devrait devenir une sous-commande officiel de la CLI Docker ; on pourra alors faire docker compose up -d
jq
pour les données relationelles. Du SQL ou des fichiers Excel/CSV/JOSN/XML en entrée et les mêmes formats en sortie (et un peu plus).vector top
, la source internal_logs
et l’API GraphQL. Un guide de mise à jour vers la nouvelle syntaxe est disponible.La prochaine édition de Time Series France aura lieu le mardi 30 Mars à 18h avec la présentation de la base StuteoDB, basée sur Apache Cassandra. Par ici pour les détails et inscriptions.
Suite de notre épopée :
Nous allons voir aujourd’hui comment présenter ces données à l’aide de Discovery, la solution de Dashboard as Code pour Warp 10 fournie par SenX.
Tout est décrit dans le billet Truly Dynamic Dashboards as Code
Dans mon cas, warp 10 est dans une partition dédiée /srv/warp10
- warp 10 est donc installé dans /srv/warp10/warp10
. C’est la valeur de $WARP10_HOME
.
Pour la configuration du plugin HTTP, j’ai un fichier $WARP10_HOME/etc/conf.d/80-discovery.conf
contenant :
# Load the HTTP Plugin
warp10.plugin.http = io.warp10.plugins.http.HTTPWarp10Plugin
# Define the directory where endpoint spec files will reside
http.dir = /srv/warp10/discovery
# Define the host and port the plugin should bind to
http.host = 127.0.0.1
http.port = 8081
# Expose the Directory and Store so FETCH requests can be performed via the plugin
egress.clients.expose = true
Le plugin HTTP sera donc accessible via une url de base en http://127.0.0.1:8081/
J’ai ensuite créé le fichier /srv/warp10/discovery/discovery.mc2
où /srv/warp10/discovery
est la valeur associée à http.dir
dans le fichier précédent.
{
'path' '/discovery/'
'prefix' true
'parsePayload' true
'macro' <%
'cerenit/dashboards/' @senx/discovery/dispatcher
%>
}
Ce fichier indique que :
/discovery/<nom_du_dashboard>
ou /discovery/<dossier_ou_arborescence>/<nom_du_dashboard>
$WARP10_HOME/macros/cerenit/dashboards
. Il s’agira de fichier WarpScript ou Flows avec l’extension en .mc2
.Avec ces deux fichiers, nous savons maintenant que :
http://127.0.0.1:8081/discovery/<nom_du_dashboard>
.$WARP10_HOME/macros/cerenit/dashboards/mon_dashboard.mc2
sera accessible via http://127.0.0.1:8081/discovery/mon_dashboard
.Un dashboard se décompose en différentes parties. Celle contenant les données a le mot clé tiles
et contient différente tile
. Chaque tile
affiche un graphique, un zone de texte, un titre ou tout composant warpView. Pour le reste, on s’appuiera sur le template par défaut.
Donc créeons un fichier $WARP10_HOME/macros/cerenit/dashboards/comptabilite/compta1.mc2
contenant :
<%
{
'tiles' [
{
'type' 'display'
'w' 4 'h' 1 'x' 3 'y' 0
'data' 'Compta - Exemple 1'
}
{
'type' 'line'
'w' 4 'h' 2 'x' 1 'y' 3
'data' [
@cerenit/accountancy/revenue
'revenue' STORE
$revenue
]
}
{
'type' 'line'
'w' 4 'h' 2 'x' 5 'y' 3
'data' [
@cerenit/accountancy/expense
'expense' STORE
$expense
]
}
{
'type' 'line'
'w' 4 'h' 2 'x' 3 'y' 5
'data' [
$revenue $expense -
]
}
]
}
@senx/discovery/render
%>
Comme indiqué précédemment, je me focalise sur le contenu de tiles
. La grille de présentation des dashboards est fixé à 12 colonnes par défaut.
Ici, je cherche donc à afficher 4 éléments :
@cerenit/accountancy/xxx
. Je pourrais mettre du code Warpscript directement dans le fichier comme dans l’exemple. Toutefois, le code exécuté dans le dashboard est visible dans le navigateur. Dans la mesure où mes requêtes pour récupérer les données demandent de l’authentification avec un passage de token, je déporte ce code dans une macro et je ne fais donc qu’appeler cette macro. Ainsi, le code sera généré coté serveur et seul le résultat sera retourné dans le navigateur.@senx/discovery/render
pour générer le dashboard.Revenons sur nos macros ; Warp 10 permet d’avoir des macros exécutées coté serveur. Ces macros peuvent être utiles pour créer/partager du code, elles peuvent prendre des paramètres en entrée si besoin et elles sont exécutées coté serveur. Dans notre cas, pour éviter que nos tokens se balladent dans le navigateur comme indiqué précédemment, c’est cette propriété qui va nous intéresser.
La macro @cerenit/accountancy/revenue
se trouve donc dans le fichier $WARP10_HOME/macros/cerenit/accountancy/revenue.mc2
et contient :
<%
{
'name' 'cerenit/accountancy/revenue'
'desc' 'Provide revenue'
} INFO
// Actual code
SAVE 'context' STORE
'<readToken>' 'readToken' STORE
[ $readToken 'revenue' { 'company' '=cerenit' } NOW [ 2016 12 1 ] TSELEMENTS-> ] FETCH
0 GET
$context RESTORE
%>
'macro' STORE
$macro
Je ne vais pas m’étendre sur la rédaction des macros mais succintement :
La macro @cerenit/accountancy/expense
est sur le même modèle en remplaçant revenue
par expense
.
Ces deux macros nous retournent donc chacune une série temporelle sur la période 12/2016 jusqu’à ce jour : une pour le chiffre d’affaires, une pour les dépenses.
Si vous allez sur http://127.0.0.1:8081/discovery/comptabilite/compta1
, vous verrez le dashboard suivant :
Le template par défaut est assez minimaliste et on note la présence d’un logo SenX. Je n’ai rien contre, mais comme c’est la compatabilité de mon entreprise que je présente, autant changer cet aspect des choses.
Pour continuer progressivement, nous allons :
title
et description
en début de fichierfooter
template
On met cela dans un nouveau fichier $WARP10_HOME/macros/cerenit/dashboards/comptabilite/compta2.mc2
.
<%
{
'title' 'Comptabilité CerenIT'
'description' 'Comptabilité CérénIT depuis 2016'
'tiles' [
{
'type' 'display'
'w' 4 'h' 1 'x' 4 'y' 0
'data' 'Compta - Exemple 2'
}
{
'title" "Chiffre d'affaires'
'type' 'line'
'w' 4 'h' 2 'x' 2 'y' 3
'data' [
@cerenit/accountancy/revenue
'revenue' STORE
$revenue
]
}
{
'title' 'Dépenses'
'type' 'line'
'w' 4 'h' 2 'x' 6 'y' 3
'data' [
@cerenit/accountancy/expense
'expense' STORE
$expense
]
}
{
'title' 'Résultat'
'type' 'line'
'w' 4 'h' 2 'x' 4 'y' 5
'data' [
$revenue $expense -
]
}
]
'footer' '<p style="text-align: center;">CérénIT © 2021 - Réalisé avec Discovery et Warp 10 de SenX</p>'
'template'
<'
<!DOCTYPE html><html><head><title id="pageTitle"></title>
{{{CSS}}}
{{{HEAD}}}
</head>
<body>
<div class="heading">
<div class="header"><h1 id="title" class="discovery-title"></h1><p id="desc" class="discovery-description"></p></div>
</div>
{{{HEADER}}}
{{{GRID}}}
{{{FOOTER}}}
{{{JS}}}
</body></html>
'>
}
@senx/discovery/render
%>
Si les propriétés title
, description
et footer
vont de soi, pour trouver comment supprimer le logo SenX, il m’a fallu lire le contenu de la macro @senx/discovery/html pour mieux comprendre les différents placehoders et leur fonctionnement.
Si vous allez sur http://127.0.0.1:8081/discovery/comptabilite/compta2
, vous verrez le dashboard suivant :
A ce stade, on note que les propriétés title
de chaque graphique n’est pas affiché. En dehors de ça, nous retrouvons bien tous nos éléments ajustés.
Néanmoins, cette lecture de @senx/discovery/html permet de voir que l’on a pas mal de points d’entrée pour rajouter des éléments spécifiques. Le tout sera de veiller à ne pas impacter les composants graphiques WarpView dans leur sémantique pour ne pas créer de dysfonctionnement.
Pour finir ce tutoriel, nous allons :
line
à spline
pour les trois graphiques déjà réalisés (pour les autres modes de réprésentation, voir les options de chart)bar
.On met cela dans un nouveau fichier $WARP10_HOME/macros/cerenit/dashboards/comptabilite/compta3.mc2
.
<%
{
'title' 'Comptabilité CerenIT'
'description' 'Comptabilité CérénIT depuis 2016'
'tiles' [
{
'type' 'display'
'w' 4 'h' 1 'x' 4 'y' 0
'data' 'Compta - Exemple 3'
}
{
'title' 'Chiffre d\'affaires'
'type' 'spline'
'w' 4 'h' 2 'x' 2 'y' 3
'data' [
@cerenit/accountancy/revenue
'revenue' STORE
$revenue
]
}
{
'title' 'Dépenses'
'type' 'spline'
'w' 4 'h' 2 'x' 6 'y' 3
'data' [
@cerenit/accountancy/expense
'expense' STORE
$expense
]
}
{
'title' 'Résultat'
'type' 'spline'
'w' 4 'h' 2 'x' 4 'y' 5
'data' [
$revenue $expense -
]
}
{
'title' 'Consolidation annuelle'
'type' 'bar'
'w' 4 'h' 2 'x' 4 'y' 7
'data' [
[ $revenue bucketizer.sum ] @senx/cal/BUCKETIZE.byyear 1970 TIMESHIFT
[ $expense bucketizer.sum ] @senx/cal/BUCKETIZE.byyear 1970 TIMESHIFT
[ @cerenit/accountancy/result bucketizer.sum ] @senx/cal/BUCKETIZE.byyear 1970 TIMESHIFT
]
'options' { 'timeMode' 'timestamp' }
}
]
'footer' '<p style="text-align: center;">CérénIT © 2021 - Réalisé avec Discovery et Warp 10 de SenX</p>'
'template'
<'
<!DOCTYPE html><html><head><title id="pageTitle"></title>
{{{CSS}}}
{{{HEAD}}}
</head>
<body>
<div class="heading">
<div class="header"><h1 id="title" class="discovery-title"></h1><p id="desc" class="discovery-description"></p></div>
</div>
{{{HEADER}}}
{{{GRID}}}
{{{FOOTER}}}
{{{JS}}}
</body></html>
'>
}
@senx/discovery/render
%>
Pour ce dernier graphique, il est donc de type bar
. Pour le détail des requêtes, je vous renvoie à la partie 2 qui explique cela. Dans notre cas, il faut juste veiller à passer une option supplémentaires pour que timeMode
interprête la date issue de la requête comme un timestamp
et non comme une date
par défaut. D’autres options comme la gestion de la présentation en mode vertical/horizontal ou en mode “stacked” ou pas.
Si vous allez sur http://127.0.0.1:8081/discovery/comptabilite/compta3
, vous verrez le dashboard suivant :
Pour résumer ce billet, nous aovns pu voir que :
monitor.stateChanges()
et monitor.stateChangesOnly()
.Si vous êtes en manque de news, vous pouvez aller consulter (et vous abonner) aux brèves du BigData Hebdo