Représentation graphique de fonctions et exemples d'utilisation


4 août 2008

Représentation graphique de valeurs ou séries de données

A chaque fois que j'avais besoin d'afficher une courbe j'utilisais des logiciels commerciaux dédiés ou des programmes que je faisais sur le pouce mais que j'oubliais bien vite une fois leur office rempli. Fini ! J'ai à ma disposition un script PHP relativement court (ça devrait s'améliorer) qui me permet de générer des images simplement.

<?

/*
Graph de fonctions : voir en bas du source pour des exemples d'utilisations.

Pour utiliser ce module de dessins de graphes ou fonctions la fonction la plus importante est :

MathGraph_create(<données à représenter>, <chemin du fichier image produit>, <paramètres>)
*/

global  $MathGraph_fontPath;        //Le chemin vers le fichier TTF utilisé pour l'affichage des textes sur un graph.
$MathGraph_fontPath = "";

//Convertit des coordonnées logiques (x,y) du repère des fonctions à représenter en coordonnées image/pixel
function    MathGraph_funcToDisplay($x, $y, &$displayX, &$displayY, &$params)
{
    $displayX = $params["LeftMargin"]+($x-$params["LogicalXMin"])*($params["DisplayWidth"]/$params["LogicalWidth"]);
    $displayY = $params["TopMargin"] + $params["DisplayHeight"] - ($y-$params["LogicalYMin"])*($params["DisplayHeight"]/$params["LogicalHeight"]);
}

function    MathGraph_logicalMinMax(&$ValSet, &$XMin, &$XMax, &$YMin, &$YMax)
{
    $XMin = min($LogicalXs = array_keys($ValSet));
    $YMin = min($ValSet);
    $XMax = max($LogicalXs);
    $YMax = max($ValSet);
}

//Nettoie le nombre contenu dans $txt : vire les 0 à la fin de la partie décimale de la virgule
function    MathGraph_cleanNumber($txt)
{
    if (!strlen($txt))      return false;
    $DotPos = strpos($txt, ".");
    if ($DotPos === false)      return $txt;
    //
    $Limit = strlen($txt) - 1;
    while ($txt[$Limit] == "0" && $Limit > $DotPos)     $Limit--;
    if ($txt[$Limit] != ".")        $Limit++;
    $txt = substr($txt, 0, $Limit);
    return $txt;
}

//displayModes peut être une suite de mode (xcenter, ycenter etc) séparés par des '|'. Bien sûr il faut que la combinaison ait un sens.
function    MathGraph_displayText($View, $x, $y, $color, $txt, $displayModes="")
{
global  $MathGraph_fontPath;

    $DisplayX = $x;
    $DisplayY = $y;
    $BBox = imagettfbbox(12, 0, $MathGraph_fontPath, $txt);
    $Modes = explode("|", $displayModes);
    foreach($Modes as $Mode)        {
        switch ($Mode)      {
            //On calcule DisplayX de manière à ce que le centre du texte soit en $x
            case    "xcenter":
                $Width = $BBox[2] - $BBox[0];
                $DisplayX = $DisplayX - $Width/2;
                break;
            //On calcule DisplayX de manière à ce qu'il n'aille pas plus loin que $x (aligné à droite)
            case    "xright":
                $Width = $BBox[2] - $BBox[0];
                $DisplayX = $DisplayX - $Width;
                break;
            case    "ycenter":
                $Height = $BBox[1] - $BBox[7];
                $DisplayY = $DisplayY + $Height/2;
                break;
            default:    break;
        }
    }
    //
    imagettftext($View, 12, 0, $DisplayX, $DisplayY, $color, $MathGraph_fontPath, $txt);
}

//Affiche un nombre sur l'image mais en l'ayant nettoyé au préalable (enlevé les 0 à la fin qui font "sales")
function    MathGraph_displayNumber($View, $x, $y, $color, $txt, $displayMode="")
{   return MathGraph_displayText($View, $x, $y, $color, MathGraph_cleanNumber($txt), $displayMode);     }

/*Taille à représenter : la largeur pour les X ou la hauteur pour les Y, peu importe
On va déterminer un espacement qui permet segmenter la taille en au moins 2 séparations.
Cet espacement est intuitif pour les humains (ceux qui raisonnent en base 10), il ne sera jamais de 7 ou de 3, mais plutôt de 1, 2 ou 5.*/
function    MathGraph_mediumStep($size, $threshold=5)
{
    if (!$size)     $size = 1;
    //
    $SizeLog = floor(log($size, 10));
    $CurPow10 = pow(exp($SizeLog), log(10));            //Puissance de 10 courante (130=>100, 1517=>1000 etc)
    $PrevPow10 = pow(exp($SizeLog-1), log(10));         //Puissance de 10 immédiatement précédente (130=>10, 1517=>100)
    //
    $TryOuts = array($PrevPow10, $PrevPow10*2, $PrevPow10*5, $CurPow10, $CurPow10*2, $CurPow10*5);
    $PrevValue = null;
    foreach($TryOuts as $Value)     {
        //On peut mettre 3 pour les lignes grossières et 5 pour les lignes fines
        if ($size/$Value < $threshold)      return $PrevValue;
        $PrevValue = $Value;
    }
    die("MathGraph_mediumStep : je ne devrais pas être ici, mon algo est faux !");
}

function    Graph_displayValue($value)
{
    $DisplayValue = sprintf("%.08f", $value);
    if ($DisplayValue == "-0")      $DisplayValue = "0";
    return $DisplayValue;
}

//Affiche une grille indiquant les unités X et Y d'un graphique
function    MathGraph_showGrid($View, $xStep, $yStep, $color, &$params, $displayCoords=true)
{
    $XMin = $params["LogicalXMin"];
    $XMax = $params["LogicalXMax"];
    $StartX = $XMin + $xStep * (ceil(($XMax - $XMin)/2)/$xStep);
    $YMin = $params["LogicalYMin"];
    $YMax = $params["LogicalYMax"];
    $StartY = $YMin + $yStep * (ceil(($YMax - $YMin)/2)/$yStep);
    //
    $XLeft = $StartX;       //-$xStep;
    $XRight = $StartX + $xStep;
    $YBottom = $StartY;     //-$yStep;
    $YTop = $StartY + $yStep;
    while (true)        {
        $Clip = 0;
        $TextYPos = $params["height"]-$params["BottomMargin"] + 14;
        $TextXPos = $params["LeftMargin"] - 14;
        MathGraph_funcToDisplay($XLeft, $YTop, $DisplayX, $DisplayY, $params);
        if ($DisplayX < $params["width"]-$params["RightMargin"] && $DisplayX>$params["LeftMargin"])     {
            ImageLine($View, $DisplayX, $params["TopMargin"], $DisplayX, $params["height"]-$params["BottomMargin"], $color);
            if ($displayCoords)     MathGraph_displayNumber($View, $DisplayX, $TextYPos, $StrongGridColor, Graph_displayValue($XLeft), "xcenter");
        }   else    $Clip++;
        if ($DisplayY < $params["height"]-$params["BottomMargin"] && $DisplayY>$params["TopMargin"])    {
            ImageLine($View, $params["LeftMargin"], $DisplayY, $params["width"]-$params["RightMargin"], $DisplayY, $color);
            if ($displayCoords)     MathGraph_displayNumber($View, $TextXPos, $DisplayY, $StrongGridColor, Graph_displayValue($YTop), "xright|ycenter");
        }   else    $Clip++;
        MathGraph_funcToDisplay($XRight, $YBottom, $DisplayX, $DisplayY, $params);
        if ($DisplayX < $params["width"]-$params["RightMargin"] && $DisplayX>$params["LeftMargin"])     {
            ImageLine($View, $DisplayX, $params["TopMargin"], $DisplayX, $params["height"]-$params["BottomMargin"], $color);
            if ($displayCoords)     MathGraph_displayNumber($View, $DisplayX, $TextYPos, $StrongGridColor, Graph_displayValue($XRight), "xcenter");
        }   else    $Clip++;
        if ($DisplayY < $params["height"]-$params["BottomMargin"] && $DisplayY>$params["TopMargin"])    {
            ImageLine($View, $params["LeftMargin"], $DisplayY, $params["width"]-$params["RightMargin"], $DisplayY, $color);
            if ($displayCoords)     MathGraph_displayNumber($View, $TextXPos, $DisplayY, $StrongGridColor, Graph_displayValue($YBottom), "xright|ycenter");
        }   else    $Clip++;
        //
        if ($Clip == 4)     break;  //Si on n'a affiché aucune unité c'est qu'on est sorti du repère, ça ne sert à rien de continuer.
        $XLeft -= $xStep;
        $XRight += $xStep;
        $YTop += $yStep;
        $YBottom -= $yStep;
    }
}

//Transforme une couleur de type "FF00FF" en un tableau array(255, 0, 255).
function    MathGraph_parseColor($strColor)
{
    if (strlen($strColor)!=6)       return false;
    $Red = hexdec(substr($strColor, 0, 2));
    $Green = hexdec(substr($strColor, 2, 2));
    $Blue = hexdec(substr($strColor, 4, 2));
    return array($Red, $Green, $Blue);
}

function    MathGraph_time()
{
    list($usec, $sec) = explode(" ", microtime(true));
    return $sec+$usec;
}

//Calcul des largeur et hauteur logiques
function    Graph_computeExtents(&$params)
{
    $params["LogicalWidth"] = $params["LogicalXMax"] - $params["LogicalXMin"];
    if (!$params["LogicalWidth"])       $params["LogicalWidth"] = 1;
    $params["LogicalHeight"] = $params["LogicalYMax"] - $params["LogicalYMin"];
    if (!$params["LogicalHeight"])      $params["LogicalHeight"] = 1;
}

//TODO : Documenter le tableau params que l'on peut passer à la fonction pour customiser l'apparence du rendu produit.
//$func est un tableau de tableaux, où chaque sous-tableau contient les données à représenter et ce sous deux formes possibles :
//- forme simple : les sous-tableaux contiennent les données uniquement.
//- forme avancée : les sous-tableaux contiennent des paramètres à des indices prédéfinis (pour l'instant "color" et "list") dont les données à représenter à l'indice "list".
function    MathGraph_create($func, $picPath, $params=null)
{
    $Start = MathGraph_time();
    if (!strlen($picPath))      return false;
    //Inits
    if ($params == null)        $params = array();
    if (!isset($params["width"]))       $params["width"] = 500;
    $Width = $params["width"];
    if (!isset($params["height"]))      $params["height"] = 300;
    $Height = $params["height"];
    if (!isset($params["background-color"]))        $params["background-color"] = array(255,255,255);
    if (!isset($params["color"]))                   $params["color"] = array(0,0,255);
    if (!isset($params["origAxisColor"]))           $params["origAxisColor"] = array(255, 0, 0);
    if (!isset($params["origAxisWidth"]))           $params["origAxisWidth"] = 2;
global  $MathGraph_fontPath;
    $MathGraph_fontPath = isset($params["fontPath"]) ? $params["fontPath"] : "fonts/trebucbd.ttf";
    //
    if (!isset($params["LeftMargin"]))      $params["LeftMargin"] = 20;
    if (!isset($params["RightMargin"]))     $params["RightMargin"] = 20;
    if (!isset($params["TopMargin"]))       $params["TopMargin"] = 20;
    if (!isset($params["BottomMargin"]))    $params["BottomMargin"] = 20;
    //###TODO : faire des vérifications de cohérence sur les marges et la hauteur ou la largeur...
    $params["DisplayWidth"] = $Width - $params["LeftMargin"] - $params["RightMargin"];
    $params["DisplayHeight"] = $Height - $params["TopMargin"] - $params["BottomMargin"];
    //On alloue une image de la taille demandée
    $View = ImageCreateTrueColor($Width, $Height);
    //On alloue les couleurs qui vont être utilisées dans la suite du code
    $BackgroundColor = ImageColorAllocate($View, $params["background-color"][0], $params["background-color"][1], $params["background-color"][2]);
    ImageFilledRectangle($View, 0, 0, $Width-1, $Height-1, $BackgroundColor);
    $ForegroundColor = ImageColorAllocate($View, $params["color"][0], $params["color"][1], $params["color"][2]);
    $LightGridColor = ImageColorAllocate($View, 215, 215, 215);
    $MediumGridColor = ImageColorAllocate($View, 150, 150, 150);
    $StrongGridColor = ImageColorAllocate($View, 0, 0, 0);
    $Black = ImageColorAllocate($View, 0, 0, 0);
    $OrigAxesColor = ImageColorAllocate($View, $params["origAxisColor"][0], $params["origAxisColor"][1], $params["origAxisColor"][2]);
    //Détermination des minimas et maximas de la fonction à représenter
    for($n=0; $n<count($func); $n++)        {
        $CurFunc =& $func[$n];
        if (isset($CurFunc["list"]))        $ValSet =& $CurFunc["list"];
        else        $ValSet =& $CurFunc;
        MathGraph_logicalMinMax($ValSet, $XMin, $XMax, $YMin, $YMax);
        $params["LogicalXMin"] = $n ? min($params["LogicalXMin"], $XMin) : $XMin;
        $params["LogicalYMin"] = $n ? min($params["LogicalYMin"], $YMin) : $YMin;
        $params["LogicalXMax"] = $n ? max($params["LogicalXMax"], $XMax) : $XMax;
        $params["LogicalYMax"] = $n ? max($params["LogicalYMax"], $YMax) : $YMax;
    }
    //
    Graph_computeExtents($params);
    //
    if (isset($params["title"]))        MathGraph_displayText($View, $Width / 2, 20, $Black, $params["title"], "xcenter");
    //Les axes "intermédiaires"
    $MediumXStep = MathGraph_mediumStep($params["LogicalWidth"]);
    $MediumYStep = MathGraph_mediumStep($params["LogicalHeight"]);
    $LightXStep = MathGraph_mediumStep($MediumXStep, 3);
    $LightYStep = MathGraph_mediumStep($MediumYStep, 3);
    //Ajout d'une tolérance, d'une marge aux limites du repère logique
    $params["LogicalXMax"] += 2*$LightXStep;
    $params["LogicalXMin"] -= 2*$LightXStep;
    $params["LogicalYMax"] += 2*$LightYStep;
    $params["LogicalYMin"] -= 2*$LightYStep;
    Graph_computeExtents($params);      //Après avoir modifié les limites logiques du repère on doit recalculer son étendue.
    //Dessine la grille du repère
    MathGraph_showGrid($View, $LightXStep, $LightYStep, $LightGridColor, $params, false);
    MathGraph_showGrid($View, $MediumXStep, $MediumYStep, $MediumGridColor, $params, true);
    //Les deux axes passant par (0,0)
    MathGraph_funcToDisplay(0, 0, $X0, $Y0, $params);
    ImageSetThickness($View, $params["origAxisWidth"]);
    $DisplayVerticalOrigin = $params["LogicalXMin"] * $params["LogicalXMax"] < 0;       //Si la multiplication donne un résultat négatif c'est que l'axe des origines est visible.
    if ($DisplayVerticalOrigin)     ImageLine($View, $X0, $params["TopMargin"], $X0, $params["height"]-$params["BottomMargin"], $OrigAxesColor);
    $DisplayHorizontalOrigin = $params["LogicalYMin"] * $params["LogicalYMax"] < 0;     //Ibid.
    if ($DisplayHorizontalOrigin)   ImageLine($View, $params["LeftMargin"], $Y0, $params["width"]-$params["RightMargin"], $Y0, $OrigAxesColor);
    ImageSetThickness($View, 1);
    //Dessine les fonctions une par une sur la grille précédente
    for($n=0; $n<count($func); $n++)        {
        $CurFunc =& $func[$n];
        if (isset($CurFunc["list"]))        $ValSet =& $CurFunc["list"];
        else        $ValSet =& $CurFunc;
        //
        if (isset($CurFunc["color"]))       {
            $ColorComps = MathGraph_parseColor($CurFunc["color"]);
            $LocalForegroundColor = ImageColorAllocate($View, $ColorComps[0], $ColorComps[1], $ColorComps[2]);
        }   else    $LocalForegroundColor = $ForegroundColor;
        //Parcours toutes les valeurs de la fonction courante
        if (isset($CurFunc["width"]))       ImageSetThickness($View, $CurFunc["width"]);
        $PrevX = $PrevY = false;
        foreach($ValSet as $x=>$y)      {
            MathGraph_funcToDisplay($x, $y, $CurX, $CurY, $params);
            ImageLine($View, $PrevX === false ? $CurX : $PrevX, $PrevY === false ? $CurY : $PrevY, $CurX, $CurY, $LocalForegroundColor);
            if ($PrevX !== false)       ImageFilledArc($View, $PrevX, $PrevY, 6, 6, 0, 360, $MediumGridColor, 0);
            $PrevX = $CurX;         $PrevY = $CurY;
        }
        if ($PrevX !== false)       ImageFilledArc($View, $PrevX, $PrevY, 6, 6, 0, 360, $MediumGridColor, 0);
        ImageSetThickness($View, 1);
    }
    //On peut afficher le temps de génération de l'image (en excluant celui de la génération du fichier proprement dit)
    if ($params["showCPUTime"])     {
        $Duration = MathGraph_time() - $Start;
        MathGraph_displayText($View, 0, 15, $MediumGridColor, sprintf("%f", $Duration), "");
    }
    //On crée le fichier image et on détruit la ressource run-time qui la représente
    $PicExt = substr($picPath, strrpos($picPath, ".")+1);
    switch ($PicExt)        {
        case    "jpg":      $Res = imagejpeg($View, $picPath);  break;
        case    "png":      $Res = imagepng($View, $picPath);   break;
        default:            echo "Extension <b>$PicExt</b> inconnue.<br/>\n";   break;
    }
    imagedestroy($View);
    return true;
}

?>
Le source PHP du module destiné à générer des images à partir de données séquentielles.

Exemples d'utilisation

Sinus

Sinus+Cosinus

Données générées

Accueil