Graphismes radar/rosace en perl avec GD

Pour des audits, il m'a été demandé de générer à la vollée des graphiques de type "rosace" ou "radar". C'est l'image idéale pour certaines analyses comme les comparaisons annuelles sur base mensuelle et plein d'autres choses. L'outil de choix est donc le module GD à partir de perl.

On fournit deux séries de chiffres qu'il faut comparer. L'idée est que l'on va déterminer 3 types de zones, les zones exclusives de chacune des séries et les zones communes. La sortie sera une image en format PNG qui propose la solution la plus optimisée. Voici un exemple:

exemple

On commence par récupérer les valeurs grâce au module CGI :

$query = new CGI;
$params = $query->param("val1");

On chompe puis on split les valeurs individuelles dans un tableau. Pour des raisons de sens trigonométriques par rapport à notre sens commun de lecture des données, on inverse l'ordre des valeurs :

chomp(params);
@suite1 = reverse split / /,$params;

Une fois les deux séries traitées, on initialise le moteur GD en précisant la taille de l'image générée puis on déclare les couleurs dont on va avoir besoin, noir et blanc pour le fond et les graduations, vert et bleu pour le deux séries et un mélange bleu-vert pour les zones communes:

$im = new GD::Image(200,200);
$white = $im->colorAllocate(255,255,255);
$black = $im->colorAllocate(0,0,0);
$mid = $im->colorAllocate(0,128,128);
$blue = $im->colorAllocate(0,0,255);
$green = $im->colorAllocate(0,255,0);
$im->transparent($white);
$im->interlaced('true');

On calcule à présent π puis l'angle représenté par chacune des tranches :

$nbparam = scalar(@suite1);
$pi = atan2(1,1) * 4;
$tranche = 2 * $pi / $nbparam;

Afin de déterminer l'échelle, on scrute les séries pour chercher la valeur la plus élevée :

$max = 0;
foreach (@suite1) {
  $max = $_ if ($_ > $max);
}
foreach (@suite2) {
  $max = $_ if ($_ > $max);
}

On détermine les coordonnées de chaque points, avec la formule :

$prevx1 = (sin ($pi) / $max * $suite1[$nbparam-1] * $size / 2) + $size / 2;
$prevy1 = (cos ($pi) / $max * $suite1[$nbparam-1] * $size / 2) + $size / 2;

On précalcule les premières coordonnées puis on effectura une rotation des valeurs dans la boucle des secteurs. Une fois que l'on a calculé les 4 points, il faut effectuer un branchement selon les cas, une série supérieure à l'autre ou alors un des deux croisements des tracés possibles.

Les cas non croisés sont simples à traiter :

  if (($suite1[$i] >= $suite2[$i]) and ($prevmax1 >= $prevmax2)) {
    $poly = new GD::Polygon;
    $poly->addPt($size / 2, $size / 2);
    $poly->addPt($prevx2,$prevy2);
    $poly->addPt($xfull2,$yfull2);
    $im->filledPolygon($poly,$mid);
    $poly = new GD::Polygon;
    $poly->addPt($prevx2,$prevy2);
    $poly->addPt($xfull2,$yfull2);
    $poly->addPt($xfull1,$yfull1);
    $poly->addPt($prevx1,$prevy1);
    $im->filledPolygon($poly,$blue);
  } elsif (($suite2[$i] >= $suite1[$i]) and ($prevmax2 >= $prevmax1)) {
    $poly = new GD::Polygon;
    $poly->addPt($size / 2, $size / 2);
    $poly->addPt($prevx1,$prevy1);
    $poly->addPt($xfull1,$yfull1);
    $im->filledPolygon($poly,$mid);
    $poly = new GD::Polygon;
    $poly->addPt($prevx1,$prevy1);
    $poly->addPt($xfull1,$yfull1);
    $poly->addPt($xfull2,$yfull2);
    $poly->addPt($prevx2,$prevy2);
    $im->filledPolygon($poly,$green);

Vient maintenant le calcul des zones croisées. pour détermier le point d'appui, sommet du polygone que constitue la zone d'intersection, on a recours à du calcul vectoriel. Accédez à la démonstration en cliquant ici. Voila le détail du premier cas:

Calcul d'alpha et beta :

    $alpha = $suite1[$i] / $suite2[$i];
    $beta = $prevmax2 / $prevmax1;

Calcul de lambda et gamma :

    $gamma = ($alpha - ($alpha * $beta)) / (1 - ($beta * $alpha));
    $lambda = 1 - ($gamma / $alpha);

Calcul du vecteur CB (CO+OB) :

    $xvco = ($size / 2) - $xfull1;
    $yvco = ($size / 2) - $yfull1;
    $xvob = $prevx1 - ($size / 2);
    $yvob = $prevy1 - ($size / 2);
    $xvcb = $xvco + $xvob;
    $yvcb = $yvco + $yvob;

Calcul du vecteur OM = OC + lambda CB :

    $xvom = $xfull1 - ($size / 2) + ($lambda * $xvcb);
    $yvom = $yfull1 - ($size / 2) + ($lambda * $yvcb);

Donc les coordonnées du point sont :

    $xxx = $xvom + ($size / 2);
    $yyy = $yvom + ($size / 2);

On dessine les zones exclusives :

    $poly = new GD::Polygon;
    $poly->addPt($xxx,$yyy);
    $poly->addPt($prevx1,$prevy1);
    $poly->addPt($prevx2,$prevy2);
    $im->filledPolygon($poly,$blue);
    $poly = new GD::Polygon;
    $poly->addPt($xxx,$yyy);
    $poly->addPt($xfull1,$yfull1);
    $poly->addPt($xfull2,$yfull2);
    $im->filledPolygon($poly,$green);

Puis la zone collective :

    $poly = new GD::Polygon;
    $poly->addPt($size / 2, $size / 2);
    $poly->addPt($prevx2,$prevy2);
    $poly->addPt($xxx,$yyy);
    $poly->addPt($xfull1,$yfull1);
    $im->filledPolygon($poly,$mid);

Reste maintenant à superposer sur le graphique une grille avec une sous graduation aux cinquièmes de la valeur maximale:

$curangle = $pi;
$prevangle = $pi;
for $i (0..$nbparam-1) {
  $curangle += $tranche;
  $xmag = (sin ($curangle) * $size / 2) + $size / 2;
  $ymag = (cos ($curangle) * $size / 2) + $size / 2;
  $im->line($size/2, $size/2, $xmag, $ymag, $black);
  foreach $qu (0.2 , 0.4, 0.6, 0.8, 1) {
    $thex = (sin ($curangle) * $size / 2 * $qu) + $size / 2;
    $they = (cos ($curangle) * $size / 2 * $qu) + $size / 2;
    $thexo = (sin ($prevangle) * $size / 2 * $qu) + $size / 2;
    $theyo = (cos ($prevangle) * $size / 2 * $qu) + $size / 2;
    $im->line($thex,$they,$thexo,$theyo,$black);
  }
  $prevangle = $curangle;
}

Le graphique est à présent fini dans sa conception, il ne reste plus qu'à renvoyer l'image, c'est à dire les données binaires, encodées en format png, ce qui implique les headers qui précèdent les données:

binmode STDOUT;
print "Content-type: image/png\n";
print "Pragma: no-cache\n";
print "Expires: now\n\n";

print $im->png;

Démonstration: Entrez les deux séries, les valeurs séparées par des espaces

radar.zip

menu principal