Génération de graphiques en langage C
Comme pour la génération des bargraphes, un premier fichier
gen-serie-graphiques.c contient le programme principal qui
permet de générer les graphiques à partir du fichier CSV local et un
deuxième fichier
gengraphe.c contient les fonctions nécessaires
pour créer les graphique d'abord en mémoire, puis dans un fichier bmp.
Génération d'une série de graphiques
Sommairement, le programme traite ligne par ligne le fichier CSV
local.
Le premier champ de chaque ligne indique sur quelle image de la série on
travaille. Si le contenu de ce champ change entre 2 lignes consécutives
du fichier CSV, c'est que toutes les données concernant le tracé des
courbes dans une image ont été traitées. On peut donc enregistrer l'image
qu'on a préparée dans un fichier BMP et passer à l'image suivante.
Le données de vaccination commencent dans les derniers jours de décembre
2020, mais on ne trace le courbes qu'à partir de janvier 2021.
Toutes les opérations destinée à préparer l'image de base (sans les
courbes), puis à enregistrer dans un fichier une image complète sont
réalisées par une série de fonctions décrites ci-dessous.
L'ajout des points de chaque courbe est fait directement sans appel de
fonction en modifiant chaque fois une case du tableau qui mémorise
l'image.
La palette de couleurs utilisée
Les différentes couleurs utilisées (numérotées à partir de 0) sont les
suivantes :
// palette des couleurs (gris clair, noir, blanc, gris, orange,
// vert jaune, vert, bleu vert, bleu, violet)
long palette [] = {0xF0F0F0, 0x000000, 0xFFFFFF, 0xB0B0B0, 0xFF6000,
0xC08000, 0x00A000, 0x0080FF, 0x2020FF, 0x8000FF};
0 | gris très clair |
teinte de fond de l'image en l'absence d'une autre couleur |
1 | noir |
tracé des axes de l'image et écriture des légendes |
2 | blanc |
teinte de fond de la partie de l'image contenant les courbes |
3 | gris clair |
teinte du quadrillage dans la partie de l'image contenant
les courbes |
4 et + | couleur vive |
couleurs des différentes courbes |
Création et initialisation d'une zone mémoire contenant une image
La fonction
cree_base_image () alloue et initialise une
zone mémoire destinée à contenir les images avec des courbes qui montrent
l'évolution au cours du temps de l'injection des différentes doses de
vaccin.
Ces images sont mémorisées à raison d'un octet par pixel dans un tableau
à deux dimensions.
Cette fonction initialise aussi la partie de l'image qui reste inchangée
pour toutes les images de la série.
Si la hauteur de l'image est fixe, sa largeur augmente au fur et à mesure
des jours qui s'écoulent. Chaque jour est représenté par un point dans
chaque courbe et nécessite donc un pixel dans la largeur de l'image.
La fonction
time () renvoie le nombre de secondes écoulées
depuis le 1er janvier 1970 à 0 H UTC. On peut l'utiliser pour compter les
jours dans le calendrier (86400 secondes par jour).
Il faut réserver des pixels à gauche du graphique pour l'échelle des
pourcentages.
Enfin, une ligne dans un fichier bmp doit occuper un multiple de 4 octets.
En raison de 2 octets par pixels, on choisira un nombre de pixels en
largeur multiple de 8.
// largeur de l'image en pixels
largeur = (time (0) / 86400) - jourdeb;
// on ajoute une marge à gauche et la largeur doit être multiple de 8
largeur = largeur + 32 - (largeur % 8);
On a ce qui faut pour générer l'entête du fichier bmp.
// fabriquer l'entête bmp
gen_entete_bmp (largeur, hauteur, 7);
Après quoi, pour chaque ligne de l'image, on alloue un tableau d'octets
qui mémorisera le contenu de la ligne à raison d'un octet par pixel.
Au départ, tous les pixels de chaque ligne sont colorés en gris très
clair (teinte de fond de l'image).
On peut remarquer que si pour allouer la mémoire, on manipule des
tableaux de lignes (tableaux de tableaux d'octets), pour accéder aux
pixels de l'image, on peut manipuler directement un tableau d'octets
à deux dimensions.
// pour chaque ligne des images
for (i = 0; i < hauteur; i++)
{
// allouer la mémoire pour les pixels de la ligne
image [i] = malloc (largeur);
// pour chaque pixel de la ligne
for (j = 0; j < largeur; j++)
// colorer ce pixel en gris très clair
image [i][j] = 0;
}
On trace à présent en bas de l'image un axe horizontal qui représentera
l'échelle des temps et du coté gauche un axe vertical qui représentera
l'échelle des pourcentages de vaccination.
Ces axes sont de couleur noire.
// tracé de l'axe vertical
for (i = haut_axe_y; i < bas_axe_y; i++)
image [i][p_axe_x] = 1;
// tracé de l'axe horizontal
for (j = p_axe_x; j < largeur; j++)
image [bas_axe_y][j] = 1;
Sur l'axe vertical, on va repérer les pourcentages multiples de 20 en
traçant des tirets horizontaux tous les 30 pixels dans le sens de la
hauteur et on écrira les pourcentages à gauche de ces tirets.
Pour écrire un pourcentage, on fabrique une chaine de caractères et on
utilise la fonction
affchaine (...) (décrite plus loin)
pour dessiner ces caractères dans l'image.
// on va rajouter une échelle des pourcentages
// pourcentage du haut qui sera indiqué en clair
p = 100;
// tous les 30 pixels en hauteur (soit 20 %)
for (i = haut_axe_y; i <= bas_axe_y; i = i + 30)
{
// tracer un tiret horizontal noir
for (j = p_axe_x - 4; j < p_axe_x; j++)
image [i][j] = 1;
// fabriquer une chaine de caractères du pourcentage
sprintf (chaine, "%d%%", p);
// l'écrire à gauche du tiret
affchaine (chaine, i - 4, 28 - (7 * strlen (chaine)));
// passer au pourcentage du dessous
p = p - 20;
}
De la même manière, sur l'axe horizontal, on va repérer les débuts de
mois en traçant des tirets verticaux en dessous desquels on indiquera
le numéro du mois écrit systématiquement avec 2 chiffres.
On utilise la fonction
affcar (...) pour dessiner ces
chiffres dans l'image.
// ajout échelle des dates
// initialisation
mois = 1;
annee = 2021;
j = p_axe_x;
// tant qu'on peut rajouter des mois
while (j < largeur)
{
// tracer un tiret vertical
for (i = bas_axe_y; i < bas_axe_y + 5; i++)
image [i][j] = 1;
// s'il y a la place à droite pour écrire le numéro du mois
if (j + 7 <= largeur)
{
// on l'écrira en dessous du tiret
i = i + 3;
// toujours sur 2 chiffres
affcar ('0' | (mois / 10), i, j - 8);
affcar ('0' | (mois % 10), i, j);
}
(...)
// passer au mois suivant
j = j + dureemois [mois - 1];
mois ++;
// si changement d'année
if (mois > 12)
// revenir en janvier
mois = 1;
}
Pour passer d'un mois à l'autre, comme la durée des mois n'est pas
constante, on a utilisé le tableau
dureemois précédemment
initialisé.
// durée des mois de l'année de 2021 à 2023
int dureemois [] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
Mais entre temps, on fait un traitement supplémentaire à chaque
changement d'année. Le trait vertical qui indique le début des mois
est prolongé vers le bas pour le mois de janvier afin d'indiquer les
changements d'années.
On veut afficher aussi le numéro de l'année. Cet affichage se fait avec
la fonction
affchaine (...) mais 3 cas se présentent pour
le positionnement du numéro d'année :
Il reste au moins une année pleine dans le graphique |
Le numéro d'année est affiché en dessous du mois de juillet |
Plus de 30 jours à afficher pour l'année en cours |
Le numéro d'année est affiché au milieu de la zone graphique
pour l'année en cours |
30 jours maximum à afficher pour l'année en cours |
Il n'y a pas assez de place pour afficher le numéro d'année |
// si premier mois de l'année
if (mois == 1)
{
// prolonger le tiret vertical
for (i = bas_axe_y + 20; i < hauteur; i++)
image [i][j] = 1;
// s'il y a la place pour rajouter l'année
if (j + 30 < largeur)
{
// fabriquer la chaine de l'année
sprintf (chaine, "%d", annee ++);
// si année complète dans le graphique
if (j + 365 < largeur)
// mettre l'année centrée sur la mois de juillet
affchaine (chaine, hauteur - 8, j + 166);
// sinon
else
// centrer l'année sur l'espace restant
affchaine (chaine, hauteur - 8, (j + largeur - 28) / 2);
}
}
La partie de l'image qu'on vient d'initialiser est commune pour tous
les graphiques qui vont être générés. La fonction qu'on vient d'étudier
n'aura pas à être exécutée une autre fois.
Initialisation de la zone de l'image qui contiendra les courbes
La fonction
init_image () initialise la partie de l'image
qui contiendra les courbes d'évolution de la vaccination en fonction du
temps.
Elle est exécutée chaque fois qu'on doit préparer une nouvelle image.
Pour commencer, on colore tous les pixels de cette zone en blanc. Si ce
n'est pas la première image qu'on génère, cette initialisation effacera
les courbes de l'image précédente.
// pour chaque ligne des images
for (i = haut_axe_y; i < bas_axe_y; i++)
// pour chaque pixel de la ligne
for (j = p_axe_x + 1; j < largeur; j++)
// colorer ce pixel en blanc
image [i][j] = 2;
Ensuite, on prolonge les tirets qui marquent les pourcentages multiples
de 20 par des traits horizontaux. Ces traits sont gris clair.
// ajout quadrillage horizontal
for (i = haut_axe_y; i < bas_axe_y; i = i + 30)
for (j = p_axe_x + 1; j < largeur; j++)
// pixel gris
image [i][j] = 3;
On fait de même en prolongeant les tirets qui marquent les débuts de
mois par des traits verticaux. Leur positionnement est plus compliqué
à cause de la durée variable des mois de l'année.
// ajout quadrillage vertical
// janvier
mois = 0;
// premier trait vertical pour le 1er février
j = p_axe_x + dureemois [0];
// tant qu'on peut tracer des traits verticaux
while (j < largeur)
{
// tracer un trait vertical
for (i = haut_axe_y; i < bas_axe_y; i++)
// pixel gris
image [i][j] = 3;
// avancer d'un mois
mois ++;
// si changement d'année
if (mois == 12)
// revenir en janvier
mois = 0;
// position du prochain trait vertical
j = j + dureemois [mois];
}
Écriture de texte dans l'image
L'insertion de texte (ici juste des chiffres et le symbole %) dans
l'image revient à réserver pour chaque caractère un rectangle composé
d'un nombre pré-défini de pixels en largeur et en hauteur, et dans ce
rectangle, à noircir certains pixels en fonction de l'aspect visuel que
devra avoir le caractère à afficher.
Pour cela, on dispose d'une variable
fonte qui contient de
quoi dessiner une liste de caractères affichables.
// dessin des chiffres et du symbole % à raison d'un bit par pixel
unsigned char fonte [11][hauteur_fonte] =
{
0x1C, 0x22, 0x22, 0x22, 0x22, 0x22, 0x1C, // 0
0x08, 0x18, 0x28, 0x08, 0x08, 0x08, 0x1C, // 1
0x1C, 0x22, 0x02, 0x04, 0x08, 0x10, 0x3E, // 2
0x1C, 0x22, 0x02, 0x0C, 0x02, 0x22, 0x1C, // 3
0x04, 0x0C, 0x14, 0x24, 0x3E, 0x04, 0x04, // 4
0x3E, 0x20, 0x20, 0x3C, 0x02, 0x22, 0x1C, // 5
0x0C, 0x10, 0x20, 0x3C, 0x22, 0x22, 0x1C, // 6
0x3E, 0x22, 0x04, 0x08, 0x10, 0x10, 0x10, // 7
0x1C, 0x22, 0x22, 0x1C, 0x22, 0x22, 0x1C, // 8
0x1C, 0x22, 0x22, 0x1E, 0x02, 0x04, 0x18, // 9
0x00, 0x42, 0xA4, 0x48, 0x12, 0x25, 0x42 // %
};
Chaque caractère affichable est ici représenté par une série de 7
octets, chacun correspondant à une ligne du dessin du caractère à
afficher, en le dessinant de haut en bas.
Le contenu de ces octets est écrit en numérotation hexadécimale, mais
la numérotation binaire montrerait mieux le contenu : chaque bit
à un dans un octet correspond à un pixel à noircir dans la ligne et
chaque bit à 0 à un pixel a laisser de la couleur du fond de l'image.
les bits de poids fort d'un octet correspondent à la partie gauche de
la ligne.
Le tableau
fonte ci-dessus est issu du code source du logiciel
libre
Cyloop
que j'ai développé et plus précisément du fichier
table_iso8859-1.c
Pour ce site web, j'ai choisi de passer dans le domaine public la partie
de cette table correspondant aux chiffres et au symbole %.
La fonction
affcar (...) permet d'afficher un caractère
dans l'image.
// rajoute dans l'image un chiffre ou le symbole %
void affcar (char caract, int lig, int col)
{
(....)
}
Elle comporte 3 paramètres d'appel :
- le caractère à afficher,
- la ligne du haut dans l'image de pour ce caractère,
- la colonne de gauche dans l'image pour ce caractère.
On commence par chercher à quel premier index du tableau
fonte
ce caractère correspond. C'est 0 à 9 pour un chiffre, 10 pour le
caractère % et les autres caractères ne seront pas affichés.
// si le caractère à insérer est un chiffre
if ('0' <= caract && caract <= '9')
// calculer sa position dans le tableau fontes
numcar = caract & 0x0F;
// sinon, si c'est le symbole %
else if (caract == '%')
// prendre sa position dans le tableau fontes
numcar = 10;
// sinon
else
// l'affichage de ce caractère n'est pas prévu
return;
Si on est toujours dans la fonction (caractère affichable), pour chaque
ligne correspondant au dessin du caractère, on récupère l'octet de
fonte qui correspond à cette ligne, on se positionne dans
l'image sur la colonne de gauche du caractère à afficher et on noircit
les bons pixels avant de descendre d'une ligne dans l'image pour pouvoir
afficher la suite.
// pour toutes les lignes du dessin du caractère
for (k = 0; k < hauteur_fonte; k++)
{
// récupérer les pixels de la ligne à noircir
valcar = fonte [numcar][k];
// se positionner sur le pixel de gauche
x = col;
// tant qu'il reste des pixels à noircir
while (valcar)
{
(....)
}
// passer à la ligne du dessous
lig ++;
}
Pour savoir si un pixel de l'image doit être noirci, on vérifie si le
bit le plus à gauche de la variable
valcar est à 1.
Ensuite, qu'on ait noirci le pixel ou non, on décale les bits de la
variable
valcar d'une position vers la gauche, on passe on
avance d'une colonne vers la droite dans l'image et on recommence le
traitement jusqu'à ce que
valcar passe à 0.
// tant qu'il reste des pixels à noircir
while (valcar)
{
// si le pixel sur lequel on est positionné doit être noirci
if (valcar & 0x80)
// le faire
image [lig][x] = 1;
// se décaler d'un pixel vers la droite
valcar = valcar << 1;
x++;
}
La fonction
affchaine (...) permet d'afficher une chaine
de caractère dans l'image.
// rajoute dans l'image la chaine de caractères passée en paramètre
void affchaine (char *chaine, int lig, int col)
{
(....)
}
Elle comporte 3 paramètres d'appel :
- la chaine caractères à afficher,
- la ligne du haut dans l'image de pour cette chaine,
- la colonne de gauche dans l'image pour cette chaine.
Son fonctionnement est simple :
- on affiche le caractère de gauche en utilisant la fonction
affcar (...)
- on se déplace de 7 pixels vers la droite dans l'image,
- on passe au caractère suivant dans la chaine,
- on avance encore de 2 pixels vers la droite si le prochain
caractère à afficher est %,
- et on continue ce traitement jusqu'à ce que tous les caractères
de la chaine soient affichés.
// tant que la cahine de caractères n'est pas affichée entièrement
while (*chaine)
{
// copier le caractère de gauche dans l'image
affcar (*chaine, lig, col);
// se positionner sur l'emplacement du caractère suivant
col = col + 7;
// passer au caractère suivant
chaine ++;
// correction si prochain caractère large
if (*chaine == '%')
col = col + 2;
}
Création d'un fichier bmp à partir de l'image en mémoire
La fonction
genimage () génère le fichier qui contiendra
une image bmp. Pour cela, elle utilise :
- l'entête bmp,
- la palette de couleurs,
- l'image mémorisée dans un tableau d'octets.
Pour cela, elle commence par ouvrir ce fichier et début de fichier
(mode
w). Si ce fichier n'existait pas encore, il est créé
vierge. Dans le cas contraire, son ancien contenu est effacé.
// génère un fichier image à partir de l'entête et du tableau de pixels mémorisé
void genimage (char *fichier)
{
char deuxpix; // groupe de 2 pixels
int i, j; // compteurs
FILE *descfic; // descripteur du fichier image
// ouvrir le fichier en écriture
descfic = fopen (fichier, "w");
// si l'ouverture s'est bien passée
if (descfic)
{
(....)
// le fichier bmp est prêt
fclose (descfic);
}
// sinon
else
// message d'erreur
fprintf (stderr, "Impossible de créer le fichier %s\n", fichier);
}
Si cette étape se passe bien, on pourra enregistrer l'image dans ce
fichier.
On commence par recopier l'entête de l'image BMP puis la palette des
couleurs.
// copier l'entête bmp
fwrite (entete, sizeof (entete), 1, descfic);
// copier la palette
fwrite (palette, sizeof (palette), 1, descfic);
À présent, il est temps de rajouter le contenu de l'image à partir du
tableau
image [ligne][colonne] qui la contient.
La copie de l'image dans un fichier bmp se fait ligne par ligne en
commençant par la ligne du bas et en remontant.
Pour chaque ligne, on recopie les pixels de gauche à droite.
Toutefois, le tableau
image [..][..] mémorise l'image à
raison d'un octet par pixel, alors que dans un fichier bmp comportant
entre 3 et 16 couleurs, l'image est mémorisée à raison de 2 pixels par
octet.
Il va donc falloir traiter les pixels 2 par 2.
// générer les lignes de l'image du bas vers le haut
i = hauteur;
// répéter
do
{
// remonter d'une ligne
i--;
// se positionner en début de ligne
j = 0;
// tant que non fin de ligne
while (j < largeur)
{
// regrouper 2 pixels sur un octet et le copier dans le fichier
(....)
// passer aux 2 pixels suivants
j = j + 2;
}
}
// jusque fin de l'image
while (i);
C'est facile à faire car les numéros de couleurs contenus dans le
tableau
image [..][..] sont suffisamment petits pour
tenir sur 4 bits.
On décale de 4 bits vers la gauche le numéro de couleur du premier
pixel et on remplace les 0 apparus à droite par le code de couleur
du pixel suivant au moyen d'un
ou sur les bits.
On aurait pu faire la même chose avec une multiplication par 16 et
une addition, mais c'est moins élégant et éventuellement moins rapide
pour l'ordinateur.
Le 2 codes de couleurs étant maintenant mémorisés dans la variable
deuxpix, il suffit de recopier son contenu dans le fichier
bmp.
// tant que non fin de ligne
while (j < largeur)
{
// regrouper 2 pixels sur un octet et le copier dans le fichier
deuxpix = (image [i][j] << 4) | image [i][j+1];
putc (deuxpix, descfic);
// passer aux 2 pixels suivants
j = j + 2;
}