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};
      
0gris très clair teinte de fond de l'image en l'absence d'une autre couleur
1noir tracé des axes de l'image et écriture des légendes
2blanc teinte de fond de la partie de l'image contenant les courbes
3gris 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 :
  1. le caractère à afficher,
  2. la ligne du haut dans l'image de pour ce caractère,
  3. 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 :
  1. la chaine caractères à afficher,
  2. la ligne du haut dans l'image de pour cette chaine,
  3. la colonne de gauche dans l'image pour cette chaine.
Son fonctionnement est simple :
    // 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 :
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;
    }