arthur.bebou

Le site arthur.bebou.netlib.re - retour accueil

git clone git://bebou.netlib.re/arthur.bebou
Log | Files | Refs |

commit 2a19baf6a4b2f4149c8286651e7441ccc21f933c
parent 2253e0e6dba2aa3e805d3ad1d82ee6ddbee890d4
Auterice: Arthur Pons <arthur.pons@unistra.fr>
Date:   Tue, 22 Oct 2024 00:05:44 +0200

Début nouvel article gnuplot

Peut-être que si je continue à faire ce genre d'articles je devrais
réfléchir à une solution pour le gitignore

Diffstat:
M.gitignore | 3+++
Acontents/git-cal/index.sh | 413+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 416 insertions(+), 0 deletions(-)

diff --git a/.gitignore b/.gitignore @@ -4,4 +4,7 @@ contents/calais-passage/*.gp contents/calais-passage/*.png contents/calais-passage/*.avif contents/calais-passage/*.sh +contents/git-cal/*.tsv +contents/git-cal/*.gp +contents/git-cal/*.png .*.sw? diff --git a/contents/git-cal/index.sh b/contents/git-cal/index.sh @@ -0,0 +1,413 @@ +#! page +title: Un exercice gnuplot +author: Arthur Pons +description: Entrainons nous à l\'utilisation de gnuplot en récréant un calendrier de l\'activité git à la gitlab +publication: 2024-10-24 + +fig() { +legend=$(< ~/git/gitcalfigures/plan sed -n "$1p") +<<. save_html main +<figure> + <img loading="lazy" src="fig$1.png" alt="$legend" /> + <figcaption> + figure $1 : $legend<br> + <a href="/calais-passage/fig$1.gp">script pour graph</a> + </figcaption> +</figure> +. +} + +section: main + +On m'a fait la remarque que mon activité sur le gitlab de l'université de +Strasbourg n'était pas bien fournie : + +![Une frise chronologique, un carré représente une journée. Plus les carrés sont +bleus plus j'ai effectué de commit ce jour là](gitlab.png) + +N'utilisant plus beaucoup cette forge je me suis demandé à quoi cette frise +ressemblerait pour les dépôts en local sur mon ordinateur. J'y ai vu un super +exercice gnuplot. + +## L'énoncé + +Admettons que l'on ait un jeu de donnée de la sorte : + + nombre-commits date + 0 1 + 0 2 + 0 3 + 1 4 2023 Oct 22 + 0 5 2023 Oct 23 + 0 6 2023 Oct 24 + 0 7 2023 Oct 25 + +En première colonne un entier, le nombre de commit, en seconde la date soit au +format "numéro de la journée dans la semaine" ou "numéro de la journée dans la +semaine, année, trois premières lettres du mois en anglais, numéro de la journée +dans le mois". Les deux colonnes sont séparées par une tabulation, les données +dans la seconde colonne par un simple espace. La dernière ligne correspond à +aujourd'hui, la première ayant une date complète à aujourd'hui - 1 ans, les +quelques lignes avant le nombre de jours nécessaires pour remonter au dernier +lundi de l'année précédentes. Ces lignes sont là pour compléter la première +semaine de l'année. + +Écrire un script gnuplot permettant de tracer un calendrier similaire à celui vu +plus tôt. + +### Pourquoi ce format un peu étrange ? + +Parce qu'il va grandement faciliter l'écriture du script. Lorsque l'on écrit du +gnuplot on est fréquemment confronté au même dilemme : pour gérer de la +complexité, est-ce que l'on devrait ajouter du code en amont de gnuplot et lui +préparer le meilleur des formats possible ou devrait-on alourdir le script +gnuplot pour qu'il gère des sources de données moins spécifiques ? En fonction de +votre objectif et de vos compétences en gnuplot la réponse peut varier. + +Ici je souhaite aborder quelques éléments de gnuplot sans trop me perdre dans +des digressions ou des points très avancés. J'ai donc opté pour une source de +données arrangeante et un script gnuplot pas *trop* compliqué. A l'inverse si +j'avais voulu explorer toute la puissance de gnuplot et distribuer un script se +suffisant à lui même je l'aurais écrit pour pouvoir lire la sortie par défaut de +`git log`. + +Le code final ainsi que le script permettant de gérer ce tableau un peu étrange +sont disponibles ici : http://git.bebou.netlib.re/git-cal/log.html + +## De 0 à 100 en 11 étapes + +Pour info, voici le résultat final : + +endsection +fig 1 +section: main + +### Étape 1 + +Conseil trivial : lorsque l'on se lance dans la création d'un graphique avec +gnuplot[^1] je suggère de toujours commencer par le graph le plus simple. Cela +permet de rapidement confirmer que : + + * l'environnement de dev et le code de base sont bons + * les données sont les bonnes + * avoir un début d'intuition de ce qu'il y a dans le jeu de données + +Ça tombe bien, c'est l'une des forces de gnuplot. Avec la ligne : + + plot 'nbofcommits.tsv' using 1 with points + +endsection +fig 2 +section: main + +On obtient instantanément un nuage de point. En abscisse le nombre de jours +depuis un an, en ordonnée le nombre de commits. On confirme visuellement que +tout semble être correct : on a une majorité de journée à zéro commits, le reste +entre 1 et 50, un gros trou durant l'été. Mais nous sommes encore bien loin de +l'arrivée. + +La commande `plot`, le cœur véritable de gnuplot, se découpe ainsi : + + * `plot` : le nom de la commande + * `nbofcommits.tsv` : le chemin du fichier à tracer + * `using` : un "modificateur" permettant de préciser quel colonne de données + tracer + * `1` : l'identifiant de la colonne à tracer + * `with` : un autre modificateur permettant d'indiquer avec quel "style" + tracer les données + * `points` : le style en question + +Si l'on met tout ça bout à bout en français, on a demandé à gnuplot : + +> Affiche la première colonne du fichier `nbofcommits.tsv` en fonction du numéro +> de ligne en traçant des points. + +Le fait que l'on retrouve en abscisse le numéro de la ligne est implicite, c'est +un comportement par défaut. En réalité le style `with points` prend deux +colonnes, `x:y`. Si `x` n'est pas précisé[^2] alors gnuplot tracera +automatiquement les données dans la colonne fournie pour `y` en fonction du +numéro de la ligne sur laquelle la donnée se trouve. Si rien n'est précisé alors +gnuplot prendra la première colonne pour x, la seconde pour y. Cela permet de +tracer rapidement un fichier tel que : + + 1 2 + 2 4 + 3 8 + 4 16 + 5 32 + +tracé avec n'importe laquelle de ces options : + + plot 'fichier.dat' with points + plot 'fichier.dat' :2 with points + plot 'fichier.dat' 1: with points + plot 'fichier.dat' 1:2 with points + +S'il n'y a qu'une seule colonne alors elle sera considéré + +### Etape 2 + +Super sauf que l'on veut des carrés ! Rien de plus simple, on peut ajouter suite +au modificateur `with` le type de point à utiliser. Il se trouve que le numéro 5 +est un carré rempli. Vous pouvez consulter les points à votre disposition en +lançant la commande `test`. Chez moi voilà ce que cela donne : + +![Une liste de styles de lignes, On remarque que le style numéro 5 a pour point +un carré remplie](types.png) + +On le précise + + plot 'nbofcommits.tsv' using 1 with points pointtype 5 + +et hop: + +endsection +fig 3 +section: main + +### Etape 3 + +A ce stade la graphique reste linéaire dans le temps et encode la quantité de +commits par les ordonnées et non pas par la couleur. Remédions d'abord à cette +histoire d'ordonnées. Notre souhait est que l'ordonnée d'un carré corresponde à +son emplacement dans la semaine. Les lundi en haut de chaque colonne, les +dimanche en bas et rebelotte pour la prochaine semaine. Heureusement[^3] notre +fichier commence un lundi et ne saute aucune journée. On a donc le pattern +suivant : + + ligne 1 -> lundi + ligne 2 -> mardi + [...] + ligne 7 -> dimanche + ligne 8 -> lundi + +Un peu d'expérience avec les maths et/ou la programmation permet de déceler +qu'il est possible de jouer avec le modulo des numéros de ligne. En effet : + + 0%7 = 0, lundi -> 0 + 1%7 = 1, mardi -> 1 + [...] + 6%7 = 7, dimanche -> 7 + 7%7 = 0, lundi -> 0 + +Pourquoi on commence par 0 plutôt que 1 ? Parce qu'en informatique on commence +souvent par zéro et que c'est le cas de gnuplot quand on lui demande le numéro +d'une ligne. Ainsi, si l'on pouvait donner en `y` à la commande `plot` non pas +la valeur dans la première colonne mais le modulo de la ligne en cours on +pourrait jouer sur la hauteur des carrés comme on le souhaite. C'est ce que +permettent la fonction `column` et l'opérateur `%` : + + plot 'nbofcommits.tsv' using (int(column(0))%7) with points pointtype 5 + +`column` permet d'accéder à ce que gnuplot appelle des "pseudo-colonnes" ou +"pseudocolumns" en anglais. Ces colonnes n'existent pas dans le fichier et sont +tenues à jour par gnuplot pour des raisons pratiques. Dans la pseudo-colonne +numéro 0 on retrouve le numéro de la ligne courante. Ainsi pas besoin de +l'ajouter à notre jeu de donnée ! On convertit ce que nous renvoie la fonction +en entier avec `int()`[^4] pour pouvoir calculer le modulo 7. On notera que la +totalité de l'opération est entre parenthèse là où auparavant on indexait les +colonnes directement avec des entiers. Ces parenthèses permettent d'introduire +une "inline transformation" ou transformation en ligne. L'expression entre les +parenthèse sera évaluée pour chaque ligne et son résultat utilisé pour la +colonne correspondante. En gros, si vous voulez faire des maths ou appeler des +fonctions à un endroit où vous auriez normalement du inscrire une valeur +particulière, utilisez des parenthèses. + +On constate qu'effectivement tous les carrés se sont rangés en sept lignes, de 0 +à 6 : + + +endsection +fig 4 +section: main + +### Etape 4 + +On pourrait penser que les carrés se sont également alignés en colonne mais +c'est une illusion. Chaque carré est un peu plus à droit que le précédent. +Pour s'en convaincre on peut élargir le graphique qui avait pour l'instant les +dimensions par défaut de gnuplot. J'opte pour une largeur conséquente de 2000 +pixels et une hauteur correspondant au ratio du nombre de semaines sur le nombre +de jours par semaine. Cela devrait nous donner un graphique final plutôt +équilibré : + + width=2000 + set term pngcairo size width,(width/(52/7)) + +La première ligne correspond à la création d'une variable. La seconde détermine +le terminal que gnuplot ciblera. Sans rentrer dans les détails, gnuplot sait +tracer pour plusieurs "terminaux" différents. Historiquement ces "terminaux" +pouvaient être de vrais terminaux physiques. Aujourd'hui cela est plutôt synonyme +de types ou de formats de sortie (png, jpeg, latex, svg etc). C'est en +choisissant le terminal que l'on peut également modifier la taille de la sortie. +J'utilise ici `pngcairo` qui fait de jolis png mais,`png` est également dispo +quoi que un peu plus vieillot pour un rendu peut-être un peu moins joli. Le +terminal `pngcairo` peut avoir tendance à générer des fichiers un peu gros mais +la marge de progression étant grande, l'utilisation d'outils type +[optipng](https://optipng.sourceforge.net/) est recommandée. + +A noter le retour des parenthèse pour faire un rapide calcul là où nous aurions +normalement du fournir un entier. + +endsection +fig 5 +section: main + +On remarque mieux le décalage horizontal des carrés. On va y remédier. + +### Etape 5 + +Comme on l'a fait pour les ordonnées, il convient de trouver le pattern adéquat +pour les abscisses. On souhaite que les paquets de 7 lignes soient toutes sur la +même colonne : + + ligne 0 -> lundi semaine 1, 0/7 -> 0 + ligne 1 -> mardi semaine 1, 1/7 -> 0,nnn + [...] + ligne 6 -> dimanche semaine 1, 6/7 -> 0,nnn + ligne 7 -> lundi semaine 2, 7/7 -> 1,nnn + ligne 8 -> mardi semaine 2, 8/7 -> 1,nnn + +On remarque que la partie entière de la division du numéro de ligne par 7 +devrait correspondre à notre besoin. En plus le fait que les numéros de lignes +commencent à 0 nous arrange ! Chouette ! La fonction `floor` va nous permettre +de répondre à notre besoin : + + x(nl) = floor(nl/7) + y(nl) = int(nl)%7 + + plot 'nbofcommits.tsv' using (x($0)):(y($0)) with points pointtype 5 + +On découvre ici les fonctions. gnuplot permet d'écrire des fonctions qui peuvent +ensuite être appelées dans différents contextes comme ici dans une +transformation en ligne. Deuxième nouveauté l'utilisation du raccourci `$0`. Il +est équivalent à `column(0)`. De manière générale quand vous voulez, à +l'intérieur d'une transformation en ligne, accéder à une valeur numérique +contenue dans la N^ième colonne alors vous pouvez écrire `column(N)` ou `$N`. + +Ainsi : + + (int(column(0)/7) est devenu + (int($0)/7) que l'on met dans une fonction pour écrire + (x($0)) + +endsection +fig 6 +section: main + +Super, nos carrés sont tous bien alignés ! + +### Etape 6 + +C'est super mais on voit pas grand chose. Puis les axes ne nous servent pas +vraiment. Puis la légende nous plus. Faisons une petite pause pour nettoyer tout +ça. Premièrement retirons les axes avec, au choix : + + set border 0 + unset border + +L'argument de `set border` paraitront étranges sauf aux personnes manipulant +le `chmod` depuis leur plus tendre enfance. En effet, il est encodé sur quatre +bits, chacun déterminant si l'un des bords est tracé ou pas. Le bit de poids le +plus faible encode l'affichage de l'axe du bas, le second celui de gauche, le +troisième celui du haut et la quatrième celui de droite. Ainsi `0110` veut dire +`afficher l'axe de gauche et celui du haut. L'argument est la représentation +décimal de la valeur, ici `8*0+1*4+1*2+1*1=6`. Il est également possible de +l'écrire `set border 2+4`. Dans notre cas rien de tout ça ne nous importe +puisque l'on ne veut rien. Dans ce cas `unset border` et `set border 0` sont +équivalents. + +Deuxièmement, retirons la légende : + + unset key + +Troisièmement retirons les "tics" : + + unset tics + +Finalement agrandissons les points en ajoutant un style après `with` : + + plot 'nbofcommits.tsv' using (x($0)):(y($0)) with points pointtype 5 pointsize 4 + +endsection +fig 7 +section: main + +Le graph ci-dessus est un collage des étapes successive. Il est possible de +réaliser des graphiques comme celui-ci avec la commande `set multiplot`. +Chaque appel succesif de la commande `plot` ajoutera un graphique supplémentaire +à la figure. La plupart du temps cela est utile pour tracer plusieurs graphs les +un à côté des autres comme ici. Pour que cela soit pratique `multiplot` peut +prendre un argument `layout` qui prendra en argument deux entiers, +respectivement le nombre de lignes et le nombre de colonnes à remplir. Ainsi le +graph ci-dessus a été construit comme ceci de façon à voir les effets des +commandes : + + set term pngcairo size width,(width/(52/7)*3) + + set multiplot layout 4,1 + + set border 0 + plot 'nbofcommits.tsv' using (x($0)):(y($0)) with points pointtype 5 + unset key + plot 'nbofcommits.tsv' using (x($0)):(y($0)) with points pointtype 5 + unset tics + plot 'nbofcommits.tsv' using (x($0)):(y($0)) with points pointtype 5 + plot 'nbofcommits.tsv' using (x($0)):(y($0)) with points pointtype 5 pointsize 4 + +Plus rarement `multiplot` permet, sans utilisation de `layout`, de tracer +n'importe où pour, par exemple, insérer un petit graph dans un autre et créer un +effet de "zoom". + +### Etape 7 + +A un moment où à un autre il va falloir s'occuper de la couleur. Il y a au moins +deux façons de faire varier la couleur de ce que l'on trace en fonction des +données dans gnuplot. + +La première est d'utiliser une colonne de couleur dédiée dont on ira chercher +les valeurs dans le fichier. L'aide de gnuplot dit : + +> When using the keywords `pointtype`, `pointsize`, or `linecolor` in a plot +> command, the additional keyword `variable` may be given instead of a number. +> In this case the corresponding properties of each point are assigned by +> additional columns of input data. Variable pointsize is always taken from the +> first additional column provided in a `using` spec. Variable color is always +> taken from the last additional column. + +Autrement dit, en plus des colonnes x et y que l'on a jusque là utilisé il en +existe d'autres qui sont utilisées pour faire varier d'autres caractéristiques +des points. Ainsi + + plot 'nbofcommits.tsv' using (x($0)):(y($0)):1 with points pointtype 5 pointsize 4 linecolor variable + +Dit à gnuplot d'aller chercher la couleur du point dans la première colonne. Or, +l'une des manières que gnuplot a d'identifier les couleurs est par des entiers +de 0 à 15[^5] faisant référence aux couleurs prédéfinies par défaut de gnuplot. +C'est d'ici que vient le superbe violet. C'est la couleur `1`. Vous pouvez voir +toutes les couleurs dans l'image test de [l'étape 2](/git-cal/#etape-2). + +endsection +fig 8 +section: main + +Les couleurs que l'on obtient ne veulent pas dire grand chose puisque l'on +indexe les 15 couleurs par défaut avec le nombre de commits durant cette +journée. Ce qu'il va nous falloir c'est un gradient de couleur qui représente +une intensité. + +### Etape 8 + +endsection +fig 9 +section: main + +cat contents/git-cal/fig3.gp + +[^1]: ou tout autre logiciel pour faire des graphs +[^2]: parce qu'il est écrit soit `:y` soit `y` tout court +[^3]: c'est à partir de maintenant que le format un peu bizarre de notre tsv + commence à nous être utile +[^4]: je suis surpris que column renvoie autre chose qu'un entier mais pas moyen + de le faire fonctionner sans la conversion. A creuser. +[^5]: qui bouclent, 16 fera référence à la même couleur que 0